プログラムを作っているとOSコマンドを実行したくなる時があります。OSコマンドの実行に問題があり、不正なコマンドをインジェクションされると大変な事になります。
どのようなセキュリティガイドラインでも「OSコマンドの実行に注意する」と大抵書かれています。
多くの場合、「コマンド実行時、コマンドと引数を分離すれば安全に実行できるAPIを利用すれば安全に実行できる」としています。
この記述は間違ってはいないです。しかし、正しくは
- コマンド実行時、コマンドと引数を分離すれば”比較的”安全に実行できる”場合が多い”
です。
コマンドと引数を分離するAPI利用だけでは完璧とは言えないセキュリティ対策です。
コマンドと引数を分離するとは?
UNIX系OSにはexecv系のコマンドを実行するシステムコールが用意されています。execv系のシステムコールは
- 実行するコマンド
- そのコマンドの引数
を別の引数として持っています。仕組的にコマンド引数は、OSコマンドとして実行できないようになっています。
完璧ですね!と言いたいのですが、完璧ではありません。
コマンド引数というコンテクスト
コマンド引数が分離されていれば引数がコマンドにはなりません。実行したコマンドに対してはコマンドになりません。
しかし、execv系システムコールに渡されたコマンド引数は、指定されたコマンドの引数として渡されます。
もし、コマンドが以下のようなシェルスクリプトだったらどうでしょうか?
#!/usr/bin/env bash lets_ls="ls $1" eval $lets_ls # 普通は # ls $1 # などと書きます。この場合は$1は丸ごと第1引数になり引数とコマンドに分割されたりしません。 # しかし、 # ls $1 # でコマンドは実行できなくても、システム上のディレクトリ内容は見放題になり得ます。もし、 # cat $1 # ならシステム上のファイルの中身を見放題になり得ます。バリデーションが欠かせません。
これを、例えばPHPのexecveシステムコールで実装されているpcntl_exec()で以下のように実行してみます。
php -r "pcntl_exec('./ls.sh', [';top']);"
topが実行されてしまいます。
pcntl_exec()がコマンド引数を分離しても、実行したコマンド(この場合、ls.sh)が引数をどのように扱うのかによって安全にコマンド実行が可能か決まります。
これはコマンドとして渡したシェルスクリプトが馬鹿すぎるからでしょう、と思うかも知れません。しかし、複雑なシェルスクリプトやプログラムが安全に引数を処理する保証はどこにもありません。コマンドとコマンド引数を分離する、という手法は完璧どころか不十分で簡単に任意コマンド実行が可能であることを示すサンプルとして十分です。
因みに、この場合はエスケープするとコマンド実行を防止できます。
php -r "pcntl_exec('./ls.sh', [escapeshellarg(';top')]);"
OSコマンドを安全に実行するには?
コマンドと引数がどのように実行されるかは
- シェル
- 実行するプログラム
に依存します。環境を固定可能であればシェルはbashに決め打ち、もできますがポータブルなプログラムの場合は不可能です。(少し分かりづらいので補足します。シェルが影響するのはシェルによってエスケープ方式が異なるからです。そもそも#!で使うシェルを指定できたりするのでシェルを固定することも難しいです)
実行するプログラムの中身が安全であると確認しないとコマンド実行されても仕方ありません。
セキュリティ対策としてエスケープは有効です。しかし、エスケープした引数であっても、実行したプログラムの引数解釈によってはコマンド実行が可能になります。エスケープをしてもシェルによってエスケープ方式が異なったりするのでエスケープも万能ではありません。
ls.shコードのコメントから理解るように、コマンド実行が行えるようなシェルスクリプトでない場合でも問題になり得ます。ファイルの内容が漏洩したり、改ざんされたりするような動作であっても(コマンド実行であっても)それが”プログラムの仕様”である場合があります。
コマンド引数がどのように処理されるのか理解した上でコマンドを実行しないと安全にコマンドを実行できません。つまり安全に動作することを保証するバリデーション無しでコマンドを安全に実行することは不可能です。
結局、OSコマンドを完璧に安全に実行するシルバーバレットは在りません。出来る限り安全に実行されるよう、コマンドと引数を分離するAPIを使っても、コマンドと引数をバリデーションするしか安全に実行する方法はないです。
コマンドと引数の分離で安全になる、とするのはプリペアードクエリを使えば安全になる、と同類の神話です。
補足
もしかすると脆弱なls.shコードを真似する青少年が出てくるかも?というコメントをFacebookで頂いたので下記のコメントをコードに追加しました。
# 普通は
# ls $1
# などと書きます。この場合は$1は丸ごと第1引数になり引数とコマンドに分割されたりしません。
# しかし、
# ls $1
# でコマンドは実行できなくても、システム上のディレクトリ内容は見放題になり得ます。もし、
# cat $1
# ならシステム上のファイルの中身を見放題になり得ます。バリデーションが欠かせません。
追加したコメントから分かるように、シェルスクリプトにコマンドインジェクション脆弱性が無くてもセキュリティ問題が発生します。ディレクトリやファイル操作など、コマンドの仕様自体、が問題の原因になることがあります。
上記の「完全なSQLインジェクション対策」で詳しく解説していますが、出力先の「コンテクスト」は1つとは限りません。階層構造になっている物もあれば、重複している物(これも階層構造とも言えますが、重なっている物、例えばHTML上のURL、CSSやJavaScript文字列コンテクスト)もあります。
コマンド実行の場合、コマンドを最初に実行するプログラミング言語のコンテクストとコマンドを実行するコンテクストが階層構造になっています。このブログの例の場合、以下のようになっています。
第1階層:
php -r “pcntl_exec(‘./ls.sh’, [‘;top’]);”
第2階層
lets_ls=”ls $1″
eval $lets_ls
コンテクストが階層構造になっていたり重複する場合、全てのコンテクストを意識/理解したセキュリティ対策を行わないと脆弱性を作ってしまいます。
例えば、今回例示した脆弱なls.shの脆弱性を
# ls $1
にして修正してコマンド実行をできなくしても、まだパス指定による任意ディレクトリの参照脆弱性が残ります。(出力を保存し取得している場合)
# cat $1
なら任意ファイル内容漏洩の脆弱性が残ります。(出力を保存し取得している場合)
このような脆弱性が残ってしまう原因は「複数のコンテクストがある出力先」を誤って「単一のコンテクストしかない」と取り扱うからです。
コマンド出力に関わらず、出力対策では「全てのコンテクスト」が重要で、「全てのコンテクスト」に対応できる対策、つまりバリデーション、が欠かせません。