명령어 주입: git ls-remote
Second-Order Command Injection (git ls-remote)
Last updated
Second-Order Command Injection (git ls-remote)
Last updated
const http = require("http");
const { execFile } = require("child_process");
const server = http.createServer((req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname === "/remote") {
const r = url.searchParams.get("r") || "";
// 취약: 사용자 입력을 그대로 원격 인자로 전달
execFile("git", ["ls-remote", r], (err, stdout, stderr) => {
res.statusCode = 200;
res.end("done");
});
return;
}
res.end("ok");
});
server.listen(3000);const express = require("express");
const { execFile } = require("child_process");
const app = express();
// 사전 등록된 안전한 리모트만 허용
const REMOTE_MAP = Object.freeze({
repoA: "https://github.com/example/repoA.git",
repoB: "[email protected]:example/repoB.git",
});
// (옵션) URL 직접 입력을 받는 경우 허용 목록 검증 + '--' 사용
function isSafeRemote(candidate) {
if (typeof candidate !== "string") return false;
if (/^-/u.test(candidate)) return false; // '-'로 시작 금지
if (/\s/u.test(candidate)) return false; // 공백/개행 금지
if (/(--upload-pack|^-u\b)/u.test(candidate)) return false; // 위험 옵션 차단
return /^(https:\/\/|ssh:\/\/|git@)[^\s]+\.git$/u.test(candidate);
}
app.get("/safe-ls-remote", (req, res) => {
const key = String(req.query.key || "");
const remote = REMOTE_MAP[key];
if (!remote) {
return res.status(400).send("invalid key");
}
// 옵션 경계 강제: '--' 뒤에 원격 인자를 둬서 옵션으로 해석되지 않도록 함
execFile(
"git",
["ls-remote", "--", remote],
{ timeout: 5000 },
(err, stdout) => {
if (err) return res.status(500).send("error");
res.type("text/plain").send(stdout);
}
);
});
app.get("/validated-ls-remote", (req, res) => {
const remote = String(req.query.remote || "");
if (!isSafeRemote(remote)) return res.status(400).send("bad remote");
execFile(
"git",
["ls-remote", "--", remote],
{ timeout: 5000 },
(err, stdout) => {
if (err) return res.status(500).send("error");
res.type("text/plain").send(stdout);
}
);
});
app.listen(3000);