説明
利用者の入力をgit ls-remoteのリポジトリ引数へ直接渡すと、-で始まる値がオプションとして解釈される可能性があります。execFileやspawnでシェルを使わなくても、Git自身によるオプションや転送方式の処理は残ります。--upload-packなどは実行するプログラムに影響します。実際に実行されるか、どこで実行されるかは、リポジトリ、転送方式、Gitの設定によって異なります。
想定される影響
- 操作されたオプションや転送方式により、意図しないプログラムが実行されるおそれがあります。
- プロセスがアクセスできるトークン、ソースコード、設定が漏えいする可能性があります。
- 未承認のホストへの接続や長時間の待機で、サービスのリソースが消費されるおそれがあります。
対処方法
- クライアントからはリポジトリの識別子を受け取り、サーバーの信頼する設定で承認済みのリモートURLに対応付けてください。
- URLを直接受け取る場合も承認済みの送信先と比較してください。プロトコルの接頭辞や
.gitの末尾だけで信頼しないでください。 - シェルを使わず引数を配列で渡し、
['ls-remote', '--', remote]のようにオプションの終わりを明示してください。 - Gitの実行ファイル、環境、設定を保護し、ネットワークアクセスと実行時間を制限してください。
--は送信先や転送方式の妥当性までは確認しません。
例
変更前
javascript
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);
変更後
javascript
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: "git@github.com: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 Object.values(REMOTE_MAP).includes(candidate);
}
app.get("/safe-ls-remote", (req, res) => {
const key = String(req.query.key || "");
const remote = Object.hasOwn(REMOTE_MAP, key) ? REMOTE_MAP[key] : undefined;
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);
変更前は入力がGitのオプションとして解釈される可能性があります。変更後の2つのルートは登録済みのリモートだけを選び、--の後に渡します。継承したオブジェクトのプロパティもリポジトリのキーとして認めません。例のURLと実行環境は実際に承認された値に置き換え、リポジトリへのアクセス権限を別途確認してください。