OSコマンドインジェクションは、Webアプリが外部から受け取った値をシェルに渡し、その値の中の記号によってサーバー上で別のコマンドまで実行されてしまう脆弱性です。2026年10月21日には、画像共有サービスGyazoを運営するHelpfeelが、動画等の変換処理で入力値がコマンドとして解釈されたことを流出の起点と公表しました。本記事では、CWE-78とIPAの定義を押さえたうえで、Python・Node.js・Goでシェルを通さずに外部コマンドを呼ぶ書き換え、引数配列でも残る引数インジェクションの塞ぎ方、画像・動画変換サーバーの構成、既存コードの点検手順までをコード例で扱います。
まとめ:OSコマンドインジェクションはシェルを通さない呼び出し方でほぼ防げる
原因は、入力値を文字列のままコマンドに連結し、シェルに解釈させていることにあります。対策の本命は入力のエスケープではありません。シェルを起動しないAPIで、実行ファイルと引数を配列として別々に渡すことです。
そのうえで、ハイフンで始まる値がオプションとして読まれる「引数インジェクション」を、許可リストの検証と--で塞ぎます。画像や動画の変換のように外部コマンドを避けにくい処理は、変換サーバーをデータベースから切り離し、乗っ取られても被害が広がらない構成にしておきます。
既存のシステムは、まず危険な呼び出しをgrepで洗い出してください。数十か所に収まるなら自社で直せる範囲です。
OSコマンドインジェクションの定義とCWE-78・IPAが示す発生条件
最初に、この脆弱性がどう定義され、どんな実装で起きるのかを確認します。
CWE-78の定義とシェルのメタ文字で命令が連結される仕組み
脆弱性の分類体系であるCWE-78は、この脆弱性を「OSコマンドに使われる特殊要素の不適切な無害化」と定義しています。CWE 4.20(2026年4月30日更新)時点で、2019年から2025年までのCWE Top 25(最も危険なソフトウェアの弱点)すべてに入っている常連です。
仕組みは単純です。シェルは;や&&、|、$( )、バッククォートを「次の命令の区切り」や「命令の埋め込み」として読みます。アプリがpingコマンドの後ろに利用者の入力を連結しているとき、入力が「example.com; cat /etc/passwd」なら、シェルはpingの後にcatも実行します。データとして渡したつもりの値が命令として読まれる構造は、SQLインジェクションとは?仕組み・攻撃例・対策をコード付きで解説で扱ったSQLの場合と同じです。
IPA安全なウェブサイトの作り方が挙げる脅威と危ない関数の一覧
IPAの安全なウェブサイトの作り方「1.2 OSコマンド・インジェクション」は、外部プログラムを呼び出せる関数を使っているすべてのWebアプリが注意の対象だとしています。例示されている関数は次のとおりです。
- PHP:exec()、passthru()、shell_exec()、system()、popen()
- Perl:open()、system()、eval()
想定される被害は、サーバー内ファイルの閲覧・改ざん・削除、ユーザーアカウントの追加、不正なプログラムのダウンロードと実行、他のシステムを攻撃する踏み台化です。攻撃者はWebサーバーの実行ユーザーの権限で動くため、Webシェルとは?Tomcatへの設置手口と検知・除去の手順で解説した不正スクリプトを置かれ、居座られる入口にもなります。
IPAが根本的解決に挙げるのは「シェルを起動できる言語機能の利用を避ける」こと。入力値のチェックは、避けられない場合の保険的対策という位置づけです。
Gyazo第三報にみる動画変換処理からデータベース流出までの経路
Helpfeelが2026年10月21日に公表したGyazo不正アクセスの調査結果(第三報)は、原因を「外部からの入力値が適切に処理されないままサーバー上のコマンドとして解釈されてしまう脆弱性」と説明しています。場所は、画像アップロードサーバーで動画等を変換する処理の一部でした。
攻撃者は変換処理用のサーバーで任意のコマンドを実行し、そこから到達できたデータベースに接続して大量のデータを読み取り、外部へ送信しています。活動が確認されたのは9月11日午後から12日未明まで。外部のソフトウェア部品や委託先に起因するものではないとも書かれています。使われたライブラリ名やCVE番号は公表されていません。
この経路は、変換処理のように「利用者が中身を決めたファイルを外部コマンドに渡す」箇所が狙われやすいこと、そしてそのサーバーからデータベースへ届く権限が被害の大きさを決めたことを示しています。利用者側の点検項目と事案の時系列はGyazoの不正アクセスと2,362万件の流出にまとめました。
脆弱なコードを引数配列での実行に書き換えるPython・Node.js・Goの例
ここからは、実際のコードで危ない書き方と直し方を並べます。題材は、入力されたホスト名にpingを打つ機能です。
Pythonのshell=Trueと文字列連結でping機能が乗っ取られる脆弱なコード
次のコードは、入力をf文字列でコマンドに埋め込み、shell=Trueでシェルに渡しています。
# 脆弱な例:入力を文字列連結してシェルに解釈させている
import subprocess
def ping(host):
cmd = f"ping -c 1 {host}"
return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout
# host = "example.com; id" が渡ると、pingの後に id も実行される
print(ping("example.com; id"))
PHPのsystem("ping -c 1 " . $_GET['host'])や、Node.jsのexec(`ping -c 1 ${host}`)も同じ構造です。どれもシェルが入力の中の;を命令の区切りとして読みます。
Pythonのsubprocessを引数リストとshell=Falseで呼ぶ書き換え
Pythonのsubprocessドキュメント(3.15系・2026年10月時点)は、このライブラリは暗黙にシェルを呼ばないため、メタ文字を含むすべての文字を子プロセスへ安全に渡せると説明しています。shell=Trueを明示したときだけ、引用符処理がアプリ側の責任になります。
# 安全な例:シェルを通さず、実行ファイルと引数を別々の要素で渡す
import re
import subprocess
HOST_RE = re.compile(r"[A-Za-z0-9.-]{1,253}")
def ping(host: str) -> str:
# 許可リストで形式を検証し、ハイフン始まりも拒否する
if not HOST_RE.fullmatch(host) or host.startswith("-"):
raise ValueError("invalid host")
result = subprocess.run(
["/usr/bin/ping", "-c", "1", "--", host],
shell=False, # 既定値だが、レビューで分かるよう明示する
capture_output=True,
text=True,
timeout=5,
)
return result.stdout
「example.com; id」は許可リストで弾かれます。仮に検証をすり抜けても、値は1つの引数としてpingに渡るだけで、;は区切りとして読まれません。実行ファイルを絶対パスで書くのは、PATHを書き換えられて別のプログラムを起動される事態を避けるためです。
Node.jsはexecからexecFileへ、Goはos/execの既定動作で防ぐ
Node.jsのchild_processドキュメント(v26系)は、exec()がシェルを起動してコマンド文字列を解釈させるのに対し、execFile()は既定でシェルを起動しないと説明しています。exec()には「サニタイズしていない利用者入力を決して渡さない」という警告が付いています。
// Node.js:execFile で実行ファイルと引数配列を渡す(shell オプションは付けない)
const { execFile } = require('node:child_process');
function ping(host, callback) {
if (!/^[A-Za-z0-9.-]{1,253}$/.test(host) || host.startsWith('-')) {
return callback(new Error('invalid host'));
}
execFile('/usr/bin/ping', ['-c', '1', '--', host], { timeout: 5000 }, callback);
}
spawn()やexecFile()にshell: trueを付けると、exec()と同じ危険に戻ります。shell: trueと引数配列の併用は、v23.11.0とv22.15.0で非推奨(DEP0190)になりました。
// Go:exec.CommandContext はシェルを経由しない
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "/usr/bin/ping", "-c", "1", "--", host).Output()
Goのos/execパッケージは、C言語のsystemと違い、意図的にシステムシェルを呼ばずglobも展開しないと明記しています。Go 1.19以降は、カレントディレクトリにある同名の実行ファイルを暗黙に拾わない挙動にも変わりました。exec.Command("sh", "-c", 文字列)と書いた時点で、この保護は失われます。
Windowsでbatとcmdを起動するときシェル経由になる例外の扱い
引数配列で渡せば安全、という原則が当てはまらない例外も存在するのです。Pythonのドキュメントは、Windowsのバッチファイル(.batと.cmd)は引数の渡し方にかかわらずOSがシステムシェルで起動しうると注意しています。Node.jsも、batとcmdはexecFile()では起動できず、cmd.exe経由になると説明しています。
結論として、利用者の入力をバッチファイルの引数にする設計は採りません。処理をバッチファイルから実行ファイルかライブラリ呼び出しへ移すか、入力を固定の選択肢から選ばせる形に変えてください。
引数配列でも残る引数インジェクションを許可リストと区切りで塞ぐ
シェルを外しても、呼び出すコマンド自身が引数をどう読むかという問題が残ります。
ハイフン始まりの入力がオプションとして解釈されるCWE-88の型
CWE-88(引数インジェクション)は、コマンドへ渡す値の中に別のオプションを紛れ込ませる弱点です。CWE-78のページも、UNIXのfindに-execのようなオプションを挿入されて実行に至る例を挙げ、CWE-88と連鎖しうると注記しています。
# シェルは通らないが、値がオプションとして読まれる例
import subprocess
url = "--directory-prefix=/var/www/html" # 攻撃者が入力したURL欄
subprocess.run(["wget", url]) # 保存先を公開ディレクトリに変えられる
OWASPのOS Command Injection Defense Cheat Sheetは、OSコマンドインジェクションは同時に引数インジェクションでもあると述べ、wgetの保存先を上書きされる例を載せています。保存先が公開ディレクトリなら、置かれたファイルがそのままWebシェルになります。
許可リストの正規表現とダブルハイフンで値をオペランドに固定する
対策は2段で組みます。1段目は許可リストの検証です。OWASPは「小文字と数字だけ、3〜10文字」を表す^[a-z0-9]{3,10}$を例に、正規表現にメタ文字と空白を含めないよう求めています。ブロックリストで;や|を消す方式は、記号の抜け漏れが出るため使いません。
2段目は--です。多くのコマンドは--の後ろをオプションではなくファイル名やホスト名として扱うため、前章のコードでは"--", hostの順に置いています。
ただし、--は万能ではありません。OWASPは、curlでは--の後ろの値もURLとして扱われ、追加の転送を防げないと注意しています。宛先を検証しないままURLを渡すと、社内向けのURLへ通信させられるSSRF(サーバーサイドリクエストフォージェリ)の問題も残ります。コマンドごとの仕様を確認し、URLならスキームとホストを許可リストで検証してください。
escapeshellargやshlex.quoteに頼る実装を採らない理由と例外
シェルを使い続けたまま値をエスケープする方法もあります。PHPのescapeshellarg()は値全体を単一引用符で囲み、1つの引数として扱わせる関数です。OWASPは、PHPならescapeshellcmd()より、こちらを優先するよう求めています。escapeshellcmd()は引数の追加を防げないためです。
それでも、新しく書くコードではエスケープ方式を採りません。理由は2つあります。1つは、エスケープしても値がハイフンで始まれば引数インジェクションが残ること。もう1つは、Pythonのshlex.quote()がドキュメント上「一部のプラットフォームでは」使えると限定されているように、OSやシェルごとに規則が違い、1か所の書き漏れで破綻することです。
例外は、パイプやリダイレクトを含むシェルスクリプトを外部から呼ぶ既存処理を、すぐには書き換えられない場合です。入力は固定の選択肢の中から選ばせ、自由入力をシェルに届かせない設計で当座をしのぎます。
画像・動画変換など外部コマンドの呼び出しを安全にするサーバー設計
アップロードされたファイルの変換は、外部コマンドを避けにくい代表的な処理です。Gyazoの事案もここで起きました。
ImageMagickのpolicy.xmlでdelegateを止め形式を限定する設定
ImageMagickは、扱えない形式を外部プログラムに渡す「delegate」の仕組みを持っています。ImageMagickのSecurity Policyは、delegateの全面無効化と、扱う形式の許可リスト化を例示しています。
<!-- policy.xml:最後に一致したポリシーが優先される -->
<policymap>
<policy domain="delegate" rights="none" pattern="*"/>
<policy domain="module" rights="none" pattern="*"/>
<policy domain="module" rights="read | write" pattern="{GIF,JPEG,PNG,WEBP}"/>
<policy domain="path" rights="none" pattern="-"/>
<policy domain="resource" name="time" value="60"/>
</policymap>
# 反映後に有効なポリシーを確認する
magick -list policy
全形式を一度拒否し、次の行でGIF・JPEG・PNG・WEBPだけを許す順序にしているのは、後に書いたルールが勝つためです。パイプ入力を拒否するpathの行は、コマンド実行が不要なら入れるよう公式に推奨されています。公開Web向けには、読み書きをGIF・JPEG・PNGに絞った「websafe」ポリシーも用意されています。
ファイル名は利用者の付けた名前を使わず、サーバー側で生成したランダムな名前に置き換えてから渡してください。拡張子ではなく先頭のバイト列で形式を判定すると、偽装したファイルを入口で落とせます。
変換サーバーをデータベースから切り離し実行ユーザーの権限を絞る構成
コードをどれだけ直しても、変換に使うライブラリ自体に脆弱性が見つかる可能性は残ります。そこで、乗っ取られる前提で構成を決めます。
- 変換処理を専用のサーバーかコンテナに分け、データベースへのネットワーク経路を持たせない
- 入力ファイルはオブジェクトストレージの一時領域から受け取り、結果も同じ領域へ書き戻すだけにする
- 実行ユーザーは書き込み先以外に権限を持たない専用ユーザーにし、外向き通信は必要な宛先だけに制限する
- 処理1件ごとにタイムアウトとメモリ上限を設け、使い捨てのコンテナで動かす
Gyazoの第三報でも、再発防止策の先頭に内部システム間のアクセス制御の強化と内部通信の暗号化が挙がっています。変換サーバーからデータベースへ届かなければ、コマンドを実行されても流出は起きません。侵入を疑ったときのログの調べ方は不正アクセスの調べ方|ログの確認方法と痕跡を見つけるコマンド例で手順化しています。
既存コードの点検手順と入力元の追跡状況から脆弱性診断を外部に任せる判断基準
最後に、手元のシステムをどう点検し、どこから外部に任せるかを条件で分けます。
grepで危険な呼び出しを洗い出しテストで注入を再現する手順
まず、外部コマンドを呼んでいる箇所を言語ごとに全数で洗い出します。
# Python:shell=True と os.system / os.popen
grep -rnE "shell=True|os\.(system|popen)\(" --include='*.py' src/
# Node.js:exec 系と shell オプション
grep -rnE "child_process|\bexec(Sync)?\(|shell:\s*true" --include='*.js' --include='*.ts' src/
# PHP:IPA が例示する関数とバッククォート演算子
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(|\`" --include='*.php' src/
ヒットした箇所ごとに、引数に利用者の入力が流れ込むかを追います。直した関数には、区切り文字を含む入力で別のコマンドが動かないことを確かめるテストを付けます。
# pytest:区切り文字入りの入力でファイルが作られないことを確認する
import pytest
from app.net import ping
def test_ping_rejects_command_separator(tmp_path):
marker = tmp_path / "injected"
with pytest.raises(ValueError):
ping(f"example.com; touch {marker}")
assert not marker.exists()
このテストは修正前のコードでは失敗し、修正後に通ります。修正前に一度落ちることを確かめておくと、テストが本当に注入を捉えているかを判断できます。
WAFやRASPを足す場面と、コード修正を先に済ませるべき条件
WAFは、;や$(を含む典型的な攻撃文字列を入口で止められます。修正版の公開まで時間がかかる製品の脆弱性に対して、時間を稼ぐ手段としては有効です。
ただし、自社コードの脆弱性をWAFで済ませる判断は採りません。アップロードされたファイルの中身やファイル名経由で変換処理に届く値は、HTTPリクエストの見た目では判定しにくいからです。アプリの内側で危険な呼び出しを止めるRASPも、コード修正の代わりではなく、修正後に重ねる層と考えてください。
入力元を追跡して自社で直せる範囲と外部に脆弱性診断を依頼すべき境界
自社で進めやすいのは、grepで見つかる呼び出しが数十か所以内で、それぞれの入力元を追える場合です。本記事の書き換えとテストの型で対応できます。
外部に任せたほうがよいのは、画像や動画の変換ライブラリを複数組み合わせている場合、入力元がアップロード・API・管理画面に分散して追い切れない場合、そして利用している製品側にOSコマンドインジェクションが見つかった場合です。製品の例では、SiteGuardの脆弱性CVE-2026-106931とはのように、更新と並行して痕跡の確認が要ります。対象のURLとアップロード機能の一覧を用意して脆弱性診断・セキュリティ診断を依頼すると、コードの点検と実際の攻撃検証をまとめて進められます。
よくある質問
OSコマンドインジェクションの対策を検討する段階で、出やすい質問に答えます。
OSコマンドインジェクションとRCE(リモートコード実行)は何が違いますか?
RCEは「外部から任意のコードを実行される」という結果を指す言葉で、OSコマンドインジェクションはそこに至る原因の1つです。RCEには、ほかにもデシリアライズの欠陥、テンプレートへの注入、アップロードしたスクリプトの実行といった経路があります。原因ごとに対策が異なるため、デシリアライズ経由の例は安全でないデシリアライゼーションとはを参照してください。
入力値から記号を取り除けば対策になりますか?
主たる対策にはなりません。;や|を消すブロックリスト方式は、改行文字や$( )、シェルごとに違う記号の抜け漏れが起きやすく、ハイフンで始まる引数インジェクションも防げません。IPAも根本的解決にはシェルを起動しない実装を挙げています。入力の検証は、許される形式だけを通す許可リストで、シェルを外した実装に重ねる形で使ってください。
フレームワークを使っていればOSコマンドインジェクションは起きませんか?
起きます。DjangoやRails、Laravelなどのフレームワークは、SQLやHTMLの出力には自動の保護を持ちますが、開発者がsubprocessやexecで外部コマンドを呼ぶ部分は守りません。画像の縮小、PDFの生成、動画の変換、圧縮ファイルの展開など、外部ツールに処理を任せた箇所がそのまま入口になります。grepでの洗い出しは、フレームワークの有無にかかわらず実施してください。
WAFを導入していればコードを直さなくてもよいですか?
自社コードの脆弱性は、コードを直すのが前提です。WAFは典型的な攻撃文字列を止められますが、アップロードしたファイルの名前やメタデータを経由して変換処理に届く値までは見分けにくく、すり抜けの余地が残ります。WAFが役立つのは、製品の修正版を待つ間の一時的な防御や、修正済みのコードに重ねる多層防御としてです。
OSコマンドインジェクションの有無はどうやって確認しますか?
自社では、外部コマンドを呼ぶ関数をgrepで全数洗い出し、利用者の入力がその引数に届くかを追う方法が確実です。直した箇所には、区切り文字を含む入力で別のコマンドが動かないことを確かめるテストを付けます。入力元が多く追い切れない場合や、運用中のシステムで攻撃が成立するかまで確かめたい場合は、脆弱性診断で実際の攻撃パターンを当てて確認します。
関連記事
- SQLインジェクションとは?仕組み・攻撃例・対策をコード付きで解説:データが命令として読まれる同じ構造を、SQLの側から解説しています。
- Gyazoの不正アクセスと2,362万件の流出|利用者の点検手順とアップロード基盤の見直し:本記事の冒頭で触れた事案の時系列と利用者側の点検項目をまとめています。
- Webシェルとは?Tomcatへの設置手口と検知・除去の手順をコマンド例で解説:コマンドを実行された後に置かれる足場の見つけ方と除去の順序を扱っています。
- 安全でないデシリアライゼーションとは?RCEに至る仕組みと言語別の防御実装を解説:コマンドインジェクション以外でRCEに至る代表的な経路です。
- 脆弱性診断とは?種類・費用相場・進め方と外注時の判断基準を解説:外部に点検を依頼するときの診断の種類と進め方を解説しています。