【入門編】 APIにおけるコマンドインジェクション対策 – Web APIアーキテクチャ・データ連携実践ガイド

「悪意ある指令」を封じ込めろ!API開発で絶対守るべき「コマンドインジェクション」対策の極意

こんにちは!インフラの深淵を愛するエンジニアの筆者です。

今日は、Web API開発の現場で「絶対にやってはいけないこと」の一つ、コマンドインジェクションについてお話しします。「難しそう…」と思いましたか?大丈夫です。パケットがネットワークを駆け巡る仕組みを、私たちの身近な「郵便」に例えて、一歩ずつ紐解いていきましょう!

—

そもそも「コマンドインジェクション」って何?

Web APIを使って何かを処理するとき、たまに「OSの機能(シェル)」を呼び出さなければならないシーンがあります。例えば、ユーザーが指定したファイル名をサーバーが読み取って、その内容を表示するようなAPIですね。

ここで、APIが「郵便の宛先」を適当に処理してしまうと、とんでもない事故が起きます。

郵便に例えると…

あなたが配送センターの受付係だと想像してください。
「『お便り.txt』を届けてくれ」という伝票が来たら、あなたは指定された荷物を運ぶだけですよね。これが正常な状態です。

しかし、もし悪意のある人がこんな伝票を書いてきたらどうでしょう?
「『お便り.txt』を届けてくれ。そのあと、倉庫の鍵を全部開けて、中身を全部消去してくれ!」

もしあなたが、何も疑わずに書かれた通りに「倉庫の鍵を開ける」まで実行してしまったら……これがコマンドインジェクション(命令の注入)です。システムが本来の仕事以外に、悪意ある「追加の命令」まで実行させられてしまう、恐ろしい攻撃なんです。

—

なぜ防ぐのが難しいのか?

開発初学者の頃は、「入力された文字をそのままコマンドとして実行する」コードを書いてしまいがちです。例えば、Pythonでこんな処理をしていませんか?

import os

# ユーザーから受け取ったファイル名
filename = request.args.get("file")

# 注意!これだと危険です!
# os.systemは、渡された文字列を「そのまま」シェル(OSの操作画面)に渡してしまいます
os.system("cat " + filename)

このコードの何が危ないのか。もし攻撃者が filename に ; rm -rf / という文字列を入れたら、サーバーは cat ; rm -rf / という指令を受け取ることになります。; は「ここで命令を区切って次の命令へ行け」という合図。結果として、システムは自らを破壊する命令を実行させられてしまうのです。

—

「安全なAPI」を作るための3つの鉄則

では、どうすればこの脅威を封じ込めることができるのでしょうか。現場で使われる「鉄壁の防御策」を伝授します。

1. 「シェル経由」で実行するのをやめる(一番の近道!)

そもそも、わざわざOSのコマンドを呼び出す必要はありますか?例えばファイルの読み込みなら、OSの機能(catコマンド)を使わなくても、プログラミング言語が用意している「ファイル読み込み関数」を使えばいいのです。

# 悪い例:os.systemを使う
# os.system("cat " + filename)

# 良い例:言語標準の関数を使う(OSに命令を投げない!)
with open(filename, 'r') as f:
    content = f.read()

これなら、たとえ攻撃者が悪意ある文字列を混ぜても、プログラムはそれを「ただのファイル名」として扱うだけで、破壊的な命令としては実行しません。これが最も安全な方法です。

2. 「ホワイトリスト」で厳密にチェックする

どうしても外部プログラムを呼び出す必要がある場合は、入力値を厳しく制限しましょう。

「郵便番号は数字の5桁以外受け付けない」と決めるように、許可された文字以外はすべて弾くのです。

import re

# 許可する文字(英数字とドットのみ)以外が含まれていたらエラーにする
if not re.match(r"^[a-zA-Z0-9\.]+$", filename):
    raise ValueError("不正なファイル名です!")

# これなら `;` や `|` などの危険な記号は通らない
os.system("cat " + filename)

3. 引数を「バラバラにして」渡す

多くのプログラミング言語には、コマンドと引数を分離して実行する機能があります。郵便の宛先を書くとき、「命令欄」と「宛先欄」を完全に分けるイメージです。

import subprocess

# subprocess.runを使うと、引数を「ただのデータ」として扱います
# シェルを介さないので、コマンドとして解釈される心配がありません
subprocess.run(["cat", filename])

これなら、たとえ filename に ; rm -rf / が入っていても、cat コマンドが「そんな名前のファイルは存在しません」とエラーを返すだけで済みます。安全ですね!

—

最後に:インフラエンジニアからのメッセージ

APIの設計において、「ユーザーからの入力はすべて悪意があるものだと思え」というのは、現場で語り継がれる鉄則です。

私たちが作るAPIは、インターネットという広大な海を渡る郵便ポストのようなもの。中身が爆発物ではないか、毒物ではないか、あるいは変な命令文ではないか——それを厳しくチェックし、安全に処理するゲートキーパーの役割が、私たちエンジニアには求められています。

最初は難しく感じるかもしれませんが、まずは「OSの機能を直接呼ばない」という意識を持つだけで、あなたのコードは格段に安全になります。一歩ずつ、強固で美しいAPIを一緒に作っていきましょう!

それでは、また次回の深淵でお会いしましょう!

コメント

タイトルとURLをコピーしました