OSコマンドインジェクション対策
シェル構文の混入と引数の意味の変更を分け、それぞれの修正方法を説明します。
説明
コマンド起動では、シェルが文字列を解釈する段階と、起動先が引数を解釈する段階があります。シェルを介さない起動に変えても、起動先のオプションとして解釈される入力は残ります。
固定のprintfと引数表示プログラムを使い、セミコロン・空白・引用符の扱いを比較します。その後、オプション終了記号を使って、ファイル名として渡す範囲を明示します。
シェル構文の混入と引数の意味の変更を分け、それぞれの修正方法を説明します。
1シェルが二つの命令を実行する様子を見る
まず固定の文字列だけを出す二つのprintfを実行します。
from sd10 import *
print(shell(/bin/shへ固定文字列を渡す"printf first; printf second"))firstsecondです。セミコロンは文字として表示されず、シェルが二つの命令へ分けました。
2同じ文字列を一つの引数へ渡す
今度はシェルを介さず、引数を表示するPythonへ渡します。
print(argv([引数配列で子プロセスを起動する"first; printf second"]))リストには一つの文字列が入っています。セミコロンも空白も、その引数の内容として渡されました。
3空白と引用符を含む値を保持する
引数配列では、値の内部にシェル向け引用符を追加する必要はありません。
print(argv(["annual report.txt"空白を含む一つの引数を渡す,"O'Reilly.txt"]))二つのファイル名がそれぞれ一要素です。シェル向けのエスケープを加えると、その記号自体が起動先へ渡る場合があります。
4引数配列でもオプションは有効になる
試料プログラムは--modeとファイル名の配列を受け取ります。
print(options(["report.txt"])既定のモードで解析する)
print(options(["--mode","admin"モードを変える引数を渡す,"report.txt"]))既定modeはread、次はadminです。シェルを使わなくても、起動先は--modeをオプションとして解釈します。
5オプションの終端を示す
--を置くと、このパーサーでは以後を位置引数として扱います。
print(options(["--"この先をファイル名の引数にする,"--mode","admin"]))modeはreadのまま、filesに--modeとadminが入ります。--を支援するか、どこに置くかは起動先の仕様に合わせます。
6固定の実行先と引数を組み合わせる
機能が受け付けるファイルを限定し、許可された名前だけを引数へ入れます。
names = {"monthly":"monthly report.txt","annual":"annual report.txt"}
selected = names["monthly"]許可された論理名から対象を選ぶ
print(argv(["--",selected])固定の起動方法に値だけを渡す)
print("unknown" in names)配列は--とmonthly report.txtだけです。入力を実行ファイル名や任意の追加オプションへ流用しない構造です。
7通常名と特殊文字をまとめて確認する
文字が保持されることと、起動先のファイル認可は別に必要です。ここでは受け取った引数を照合します。
samples = ["report.txt","a b.txt","x;y.txt","--mode"]
for value in samples:
print(options(["--",value])値のまま位置引数へ入ることを確認する)すべてmodeはread、filesには元の一文字列です。実際の処理では、この後のパス検証やアクセス権も適用します。
まとめ
シェルの構文を使わない場合は引数配列で起動します。
起動先が解釈するオプションも制限します。
実行先・操作・対象の許可範囲を分けて決めます。
操作履歴と解説を振り返れます。
Python 3.15 / 固定の公開試料
from sd10 import *
print(shell("printf first; printf second"))firstsecond
print(argv(["first; printf second"]))['first; printf second']
print(argv(["annual report.txt","O'Reilly.txt"]))['annual report.txt', "O'Reilly.txt"]
print(options(["report.txt"]))
print(options(["--mode","admin","report.txt"])){'mode': 'read', 'files': ['report.txt']}
{'mode': 'admin', 'files': ['report.txt']}print(options(["--","--mode","admin"])){'mode': 'read', 'files': ['--mode', 'admin']}names = {"monthly":"monthly report.txt","annual":"annual report.txt"}
selected = names["monthly"]
print(argv(["--",selected]))
print("unknown" in names)['--', 'monthly report.txt'] False
samples = ["report.txt","a b.txt","x;y.txt","--mode"]
for value in samples:
print(options(["--",value])){'mode': 'read', 'files': ['report.txt']}
{'mode': 'read', 'files': ['a b.txt']}
{'mode': 'read', 'files': ['x;y.txt']}
{'mode': 'read', 'files': ['--mode']}演習完了