コンテンツにスキップ

Command Injection (コマンド インジェクション)

概要

ユーザー入力から不適切に shell コマンドが作成されている場合、攻撃者は API を実行しているホストで恣意的なコマンドを実行できる可能性があります。 通常、コマンドは脆弱性がある API の権限を使用して実行されます。

コマンド インジェクションは、重大なセキュリティ脆弱性であり、データ漏洩や恣意的なコード セキュリティにつながります。

推奨事項

サブプロセスを実行するのではなく、言語やライブラリ エコシステムの既存の API を使用します。 たとえば、curl を実行するのではなく、言語の HTTP クライアント ライブラリを使用することを検討します。

上記が不可能な場合は、スペース区切りの単一の文字列ではなく引数のリストを受け取るライブラリ関数を使用します。 それによって、$(); および ` などの特殊文字シーケンスによる予期しない shell 評価を避けることができます。

また、意図されたエンドポイントの機能を維持しながら、できるかぎりユーザー入力を サニタイズすることを推奨します。 たとえば、入力がコマンドの一部として使用される場合、英数字以外の文字を禁止することを考慮するなどの対策が考えられます。

サンプル

次のサンプルには、単純な FastAPI python ファイル アップロード API があります。 API はフォルダーを使用してファイルを保存し、ユーザーはファイルをアップロード、取得、削除できます。

from fastapi import FastAPI, File, UploadFile, HTTPException
from pathlib import Path
from os import system

app = FastAPI()

STORAGE = Path("/tmp")

@app.get("/files/{file_id}")
async def read_file(file_id: int):
    try:
        return {"contents": open(STORAGE / str(file_id), "rb").read()}
    except FileNotFoundError:
        raise HTTPException(status_code=404, detail="😱 File not found")

@app.post("/files/{file_id}")
async def write_file(file_id: int, file: UploadFile = File(...)):
    contents = await file.read()
    open(STORAGE / str(file_id), "wb").write(contents)
    return "🗄️  File has been stored"

@app.delete("/files/{file_id}")
async def delete_file(file_id):
    system("rm " + str(STORAGE / file_id)) # this is bad
    return "✨ File has been deleted"

見てわかるとおり、削除エンドポイントは system を使用して rm を実行することでファイルを削除します。 しかし、攻撃者が file_id の一部としてコマンドを渡す可能性があります。

$ curl 'http://localhost:8081/files/;echo%20hello%20from%20command%20injection' -XDELETE                                                                                        16:40:27
"✨ File has been deleted"

ログを見ると、異常があるのがわかります。rm がエラーを返し、攻撃者のコマンドを出力しています。

rm: cannot remove '/tmp/': Is a directory
hello from command injection
INFO:     127.0.0.1:54716 - "DELETE /files/%3Becho%20hello%20from%20command%20injection HTTP/1.1" 200 OK

攻撃者は恣意的なコマンド echo を実行しました。このコマンドは無害ですが、 攻撃者が容易にデータを抽出したり、独自に shell を開いたりすることも可能なことは想像がつくでしょう。

上記の推奨事項に従えば、この問題を修正する方法は 複数あります。 - os.systemsubprocess.check_output(["rm", str(STORAGE / file_id)]) に変更します。 これによってインジェクションを防ぐことができます (ただしパス トラバーサルを 許す可能性はあります)。 - よりよい方法として、os.system をファイル削除用のライブラリ関数 os.remove に変更します。 これによってサブプロセスを開始する必要がなくなるので、より安全で高速です。 - file_id を検証してコマンド インジェクション ペイロード、 さらにはパス トラバーサルが含まれていないことを確認できます。

安全な削除エンドポイントは次のようになります。

@app.delete("/files/{file_id}")
async def delete_file(file_id: int):
    os.remove(STORAGE / str(file_id)) # this is ok because file_id is an integer
    return "✨ File has been deleted"

参考資料