
Webアプリケーション診断の現場で、今でもよく聞く言葉があります。
「そこは大丈夫だと思っていました」
OSコマンドインジェクションは、その“安心”の影に残りやすい脆弱性です。SQLiやXSSほど話題にならず、ツールでも見えにくい形で通り過ぎることがあります。
きっかけはとても地味です。
既存のCLIの流用、速度優先の実装、昔のスクリプトの残存――その積み重ねで、入力がシェルを介した実行パスに入り込む構造が生まれます。
この記事では、成立する構造(よくある実装)と、表に出ないブラインド型、そして設計・実装・運用での潰し方を整理します。
この記事で扱うこと(技術者向け)
- OSコマンドインジェクションが成立する「境界」の正体
- よくある構造(シェル経由/引数連結)と、表に出ないブラインド型
- 設計・実装・運用での予防(シェル回避/引数分離/権限・環境)
- 言語ごとの“残りやすい書き方”と置き換え観点
TABLE OF CONTENTS
OSコマンドインジェクションとは何か
OSコマンドインジェクション(シェルインジェクション)は、アプリケーションが受け取った入力値が、処理の途中でOSのコマンドとして解釈されてしまうことで起きる脆弱性です。
典型は、文字列連結でコマンドを組み立て、シェル経由で実行しているケースです。開発者は「引数を渡しているつもり」でも、シェルに渡った瞬間、入力は“値”ではなく“命令の一部”として扱われます。
成立すると、アプリの権限の範囲でできることが一気に増えます。
- ファイルの読み書き
- 設定変更・情報取得
- 外部通信
- 別処理の起動(実行ユーザー権限での操作)
重要なのは「入力チェックの有無」だけではなく、最終的な実行経路がシェルを介していないかまで確認することです。
なぜ深刻になりやすいのか

OSコマンドインジェクションが厄介なのは、影響範囲が広い点です。
- アプリケーションの外側、OSレベルまで操作が及ぶ
- 成功確認が容易なケースがある(ログ・挙動の差分が出る)
- 表示結果が返らなくても成立する「気づきにくい」パターンがある
“入力がデータから命令へ意味を変える境目”が一度でも成立すると、アプリの設計意図とは別に、OSの仕様どおりに処理が進みます。ここが、他の入力系バグと違う怖さです。
どうやって起きるのか:よくある構造
現場で一番よく目にするのは、内部処理でコマンドラインツールを直接呼び出しているケースです。在庫確認や画像変換、ログの集計など、「既存のCLIを使えば早い」という判断自体は珍しくありません。
問題になりやすいのは、その実装が数年後もそのまま残り続けることです。入力経路が増え、前提が崩れているのに、内部処理だけが昔のままコマンドを組み立てている、という構造はよく見かけます。
たとえば、バックエンドで次のような処理をしているケースです。
stockreport.pl 381 29
開発者の感覚としては、「381 と 29 を引数として渡しているだけ」です。ここに危険な処理があるようには、なかなか見えません。
しかし、入力値を文字列連結したままシェルを介して実行すると、状況は変わります。区切り文字が混ざった時点で、OSは“別の命令”として扱う可能性があります。
381; <INJECTED_COMMAND>
この場合、OSから見ると「381 を引数にしたコマンド」と「別のコマンド」が順番に実行されている状態になり得ます。アプリケーション側は値を渡したつもりでも、OS側では命令の列として処理される、というズレです。
ここで重要なのは、「入力を検証しているか」だけでは足りない点です。型変換の前後、文字列連結のタイミング、実行APIの選択(シェルを介するかどうか)などで、抜け道が残ることがあります。
コードを追っていくと、問題の箇所は意外と短い数行だった、ということも少なくありません。「ここは昔からこうだった」。その一言で通り過ぎてしまいがちな場所に、入口が残ります。
表に出ない「ブラインド型」がある
すべてのケースで、実行結果が画面に表示されるわけではありません。実務では、むしろ何も返らないパターンの方が多い印象です。
OSコマンドインジェクションの中には、表面上は正常に動いているように見えながら、内部では確実にコマンドが実行され得るものがあります。
時間差で気づくケース
レスポンスが不自然に遅れることで、内部で処理が走っていることが分かる場合があります。Linux と Windows では命令は異なりますが、「一定時間、処理が止まる」という現象自体が手がかりになります。
ファイルを経由するケース
実行結果が、Webから参照できる場所に書き出されてしまうケースもあります。アプリケーション側は通常どおりの応答を返していても、サーバー上に本来存在しないはずのファイルが残る。診断の現場では、そうした痕跡を後から見つけることも少なくありません。
外部通信を使うケース
DNSやHTTP通信などを発生させ、その痕跡を外部ログで確認することで成立を判断することもあります。アプリケーションのレスポンスに変化が出ないため、運用中に気づきにくいのが特徴です。
実行できてしまう操作の一例(参考)
※以下は、許可された診断・検証環境で影響範囲を切り分けるための例です。運用環境での実行を意図するものではありません。
OSコマンドインジェクションが成立すると、アプリケーションの権限で、サーバー内部の基本情報に触れられる可能性があります。以下は、許可された診断環境で“影響範囲の切り分け”に使われる代表例です。
| 目的 | Linux | Windows |
|---|---|---|
| 実行ユーザーの確認 | whoami | whoami |
| OS情報の確認 | uname -a | ver |
| ネットワーク設定 | ifconfig / ip addr | ipconfig /all |
| 通信状態の確認 | netstat -an | netstat -an |
| 実行中プロセス | ps -ef | tasklist |
よく使われる区切りと記号
コマンドが連結できてしまう背景には、シェルが特定の記号を「区切り」や「展開」として解釈する仕組みがあります。アプリケーション側では単なる文字列のつもりでも、シェルに渡った瞬間に意味が変わります。
代表的なものは次のとおりです(設計レビューの観点として把握しておくための整理です)。
;や&&&、||- パイプ
| - バッククォート
`cmd`や$(cmd)
入力値がクォートで囲まれている場合でも、必ずしも安全とは言えません。クォート自体を閉じられると、その後ろは再び命令として解釈されます。見た目上は「文字列として扱っている」ように見えても、シェルの解釈は単純ではありません。
診断の現場でよく聞くのが、「エスケープしているから大丈夫だと思っていた」という言葉です。ただ、エスケープ処理は抜けやすく、環境や実行方法の違いで解釈が変わることもあります。ここが、長く残り続ける理由の一つです。
実務で見る予防の考え方
一番確実なのは、そもそもシェルを呼ばないことです。OSコマンドを文字列として組み立てるのではなく、言語やフレームワークが提供するAPI(引数分離・型保証が効くもの)を使う。それだけで成立条件の大半は消えます。
設計として押さえる基本は、だいたい次に集約されます。
- シェルを介さない実行方式を選ぶ(引数を分離して渡す)
- 入力値は許可リストで制限する(文字種・長さ・フォーマット)
- 実行権限・実行環境を絞り、影響範囲を限定する
どうしてもシェルを使う必要がある場合
どうしてもコマンドを使う必要があるケースでは、最低限ここを外さない、という線があります。
- 引数は配列として渡し、シェルを介さない
- 入力値はホワイトリストで制限する(許可した文字種のみ)
- 数値は数値として検証する(文字列前提で扱わない)
- IDや名前は、許可した形式だけを通す(正規化+厳格化)
ブラックリスト方式や簡易的な文字置換に頼った実装は、回避されている例を何度も見てきました。「これくらいなら大丈夫だろう」という前提は長く持たないことが多い印象です。
権限と実行環境の見直し
対策はコードだけに限りません。実行環境まで含めて見直すことで、被害の上限を下げられるケースがあります。
- 実行ユーザーは最小権限になっているか
- 不要なツール・ユーティリティが残っていないか(攻撃面の削減)
- コンテナや分離環境で、影響範囲を限定できているか
OSコマンドインジェクションは、実装と環境の両方が噛み合ったときに成立します。だからこそ、コードだけを見て安心するのではなく、どの権限で、どんな環境で動いているのかを一緒に確認することが重要です。
開発言語ごとの注意点(よくある例)
OSコマンドインジェクションは特定の言語に限った問題ではありません。ただ、言語ごとに「やりやすい書き方」「残りやすい実装」があり、同じようなパターンに何度も出会います。
ここでは“便利さの裏側にある落とし穴”として、現場で多い例を整理します。
Python
文字列連結で os.system() などを呼んでいる場合は要注意です。入力値がそのままシェルを介する実行経路に入りやすい。引数を分離して渡す方式に切り替えるだけで、リスクは大きく下がります。
Node.js
exec() 系はシェルを介するため、入力値が混ざると危険になりやすいです。引数を分離できる実行方法を選ぶ、という設計判断が効きます。非同期処理との相性が良いぶん、置き換え忘れが残りやすい印象もあります。
PHP
exec() や system() に入力値を直接渡す設計は、古いコードほど残りやすい印象があります。当時は一般的だった実装が、そのまま動き続けているケースも少なくありません。可能であればコマンドを使わない設計に戻し、難しい場合でも引数・権限・実行環境をセットで見直す必要があります。
診断・レビュー時に見ているポイント

診断やコードレビューの際、特に意識して見ているのは次の点です。
- OSコマンドを呼んでいる箇所が、どこに存在しているか
- 入力値が、そのままコマンドに渡っていないか(連結/暗黙の変換)
- 想定外の文字や記号が、通過できない設計になっているか(許可リスト)
- 実行ユーザーの権限が、必要以上に広くなっていないか
- ブラインド型でも差分が取れるログ/監視があるか
これらは、どれか一つだけ見れば十分というものではありません。実装・入力・実行環境がどう噛み合っているかを、全体として確認する必要があります。
まとめ:入力値が「コマンドになる瞬間」を見直す
OSコマンドインジェクションは、特別な攻撃手法がなくても成立し得ます。入力がどこで「データ」から「命令」に変わるのか。その境界を一度だけ落ち着いて追ってみる。それだけで見えてくるものがあります。
現場で診断をしていると、その確認を事前にしていれば防げたケースに何度も出会います。派手な修正や大きな仕組み変更が必要だったわけではなく、「ここでシェルを経由していた」「この入力は想定より広かった」と気づければ十分だった、という場面です。
何かが起きてから振り返るよりも、何も起きていない今のほうが冷静に判断できます。OSコマンドインジェクションは、まさにその“平常時”に見直しやすいポイントです。






