Skip to content

ハウツー: ライブコンテナペネトレーションテスト

このガイドは、NENE2 アプリケーションに対する敵対的なライブコンテナペネトレーションテストの実行方法を — セットアップから 30 の攻撃フェーズすべてまで — 文書化し、v1.5.329 のテストセッション(2026-05-31、150 件以上のケース)からの正準の結果を記録します。

このテストはクラッカー視点を採用します: 攻撃者はソースコードへの完全なアクセス(ホワイトボックス)を持ち、すべての公開ドキュメントを読み、諦める前にあらゆる既知の攻撃クラスを試すと仮定します。


前提条件

  • Docker Compose が利用可能(docker compose version
  • ホストに curlnc(netcat)、opensslpython3 がインストールされている
  • テスト用認証情報を持つ稼働中の NENE2 コンテナ

1. コンテナのセットアップ

隔離されたテストターゲットを起動します。専用ポート(本番ポートは決して使わない)を使い、テスト用認証情報を注入します:

bash
# PHP ビルトインサーバーターゲット — 最速で起動でき、生の NENE2 の振る舞いをテストする
NENE2_MACHINE_API_KEY=pentest-key docker compose run -d --rm \
  -e NENE2_LOCAL_JWT_SECRET=pentest-jwt-secret-32chars-min!! \
  -e APP_ENV=local \
  -e APP_DEBUG=false \
  -p 8299:80 \
  app php -S 0.0.0.0:80 -t public_html/

# Apache ターゲット — Apache 設定のハードニングを含むフルスタックをテストする
NENE2_MACHINE_API_KEY=pentest-key docker compose up -d app
# :8200 で利用可能(CLAUDE.md §8 のポートレジストリ参照)

ベースラインのスモークチェック:

bash
curl -si http://localhost:8299/
# 期待: 200 OK、セキュリティヘッダーあり、Server/X-Powered-By なし

OpenAPI から攻撃面を列挙する:

bash
curl -s http://localhost:8299/openapi.php | grep -E "^  /"
# → /, /health, /machine/health, /examples/protected,
#   /examples/notes, /examples/notes/{id}, /examples/tags, /examples/tags/{id}

コンテナ内でテスト用認証情報を生成する:

bash
CID=$(docker ps --filter "publish=8299" --format "{{.ID}}")
VALID_JWT=$(docker exec $CID php -r "
  require 'vendor/autoload.php';
  \$v = new Nene2\Auth\LocalBearerTokenVerifier('pentest-jwt-secret-32chars-min!!');
  echo \$v->issue(['sub'=>'tester','exp'=>time()+86400]);
")

2. 攻撃フェーズ

フェーズ 1 — JWT アルゴリズム混同

IDAttackExpectedv1.5.329
J-01alg:none(空の署名)401✅ BLOCKED
J-02alg:NONE(大文字)401✅ BLOCKED
J-03alg:None(大小混在)401✅ BLOCKED
J-04alg:hs256(小文字)401✅ BLOCKED
J-05alg:RS256(鍵混同)401✅ BLOCKED
J-06alg フィールドなし401✅ BLOCKED
J-07kid: ../../etc/passwd200(有効な署名)✅ SAFE — 余分なヘッダーフィールドは無視
J-08jku: http://evil.com200(有効な署名)✅ SAFE — JWK フェッチなし
bash
# J-01: alg:none
H=$(echo -n '{"typ":"JWT","alg":"none"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
P=$(echo -n '{"sub":"admin","exp":9999999999}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
curl -si -H "Authorization: Bearer $H.$P." http://localhost:8299/examples/protected
# → 401  detail: "Token algorithm must be HS256."

フェーズ 1b — JWT ペイロード改ざん

IDAttackExpectedv1.5.329
J-09exp: 0(エポック 1970)401 expired✅ BLOCKED
J-10exp: null401 must be numeric✅ BLOCKED
J-11exp: "never"401 must be numeric✅ BLOCKED
J-12exp: 9999999999.9(float)401 must be numeric✅ BLOCKED
J-13ペイロードが JSON 配列401 must be numeric✅ BLOCKED
J-14Bearer 値内のダブルスペース401✅ BLOCKED
J-15Bearer スキームなし401✅ BLOCKED
J-164 セグメントトークン(余分なドット)401 format invalid✅ BLOCKED
J-17ヘッダー + ペイロードのみ(署名なし)401✅ BLOCKED

重要な不変条件: exp は存在する整数でなければならない — 不在または誤った型は拒否される(v1.5.329 で修正)。

フェーズ 2 — SQL インジェクション

すべてのリポジトリは ? プレースホルダーのパラメーター化クエリを使用します。生の文字列補間はありません。

IDAttackExpectedv1.5.329
S-01title 内のクラシックな ' OR 1=1--201(リテラルとして保存)✅ SAFE
S-02UNION SELECT 1,2,3--201(リテラルとして保存)✅ SAFE
S-03ブール盲目的 AND 1=1--201(リテラルとして保存)✅ SAFE
S-04時間ベース AND SLEEP(2)--<50ms で 201✅ SAFE — SLEEP は実行されない
S-05パスパラメーター SQLi /notes/1' OR '1'='1200(int キャスト → 1)✅ SAFE
S-06ヌルバイト \0' OR '1'='1201(リテラル)✅ SAFE
S-07セカンドオーダー: ペイロードを保存してから読む200(リテラルとして再読込)✅ SAFE
S-08ボディフィールド内の SLEEP(5)<50ms で 201✅ SAFE
S-10クエリ文字列内の limit=UNION SELECT...422(バリデーション)✅ SAFE
bash
# パラメーター化クエリを検証: SLEEP は実行されない
time curl -si -X POST -H "Content-Type: application/json" \
  -d '{"title":"x'\'' AND SLEEP(3)--","body":"x"}' \
  http://localhost:8299/examples/notes
# → < 100ms で 201(SLEEP は実行されなかった)

フェーズ 3 — パストラバーサル / LFI / PHP ラッパー

IDAttackExpectedv1.5.329
P-01../../etc/passwd404✅ BLOCKED
P-02URL エンコードされた %2e%2e%2f バリアント(5 形式)404✅ BLOCKED
P-03二重エンコードの %252e%252e404✅ BLOCKED
P-04UTF-8 オーバーロング %c0%ae404✅ BLOCKED
P-05php://input / php://filter / data://404✅ BLOCKED
P-06{id} パラメーター経由の LFI404✅ BLOCKED
P-07ヌルバイト 1%00.html200(int キャスト → 1)✅ SAFE — id=1 の DB レコードが返る
P-08Apache 上の .htaccess403✅ BLOCKED
P-08bPHP ビルトインサーバー上の .htaccess200⚠️ EXPOSED(VULN-01 参照)
P-09.git/HEAD404✅ BLOCKED
P-10バックアップファイル(.bak.swp~ など)404✅ BLOCKED

フェーズ 4 — HTTP プロトコル攻撃

IDAttackExpectedv1.5.329
H-01CL.TE リクエストスマグリングレスポンスなし(PHP ビルトインがブロック)
H-02TE.CL スマグリング405(root メソッド不一致)
H-03TE.TE 難読化 Transfer-Encodingレスポンスなし
H-04HTTP/1.0 ダウングレード200(正しいボディ)
H-05絶対 URI プロキシ悪用404
H-06HTTP ヘッダーフォールディング500(PHP ビルトインのバグ)⚠️ VULN-02
H-07HTTP パイプライニングレスポンスがインターリーブ✅ SAFE
H-08100 個の同時カスタムヘッダー200✅ SAFE
H-10WebSocket アップグレード200(アップグレードは無視)✅ SAFE
H-12無効な HTTP バージョン(HTTP/9.9200(PHP ビルトインは受理)✅ SAFE

フェーズ 5 — マスアサインメント / IDOR / ビジネスロジック

IDAttackExpectedv1.5.329
B-01マスアサインメント(ボディ内の id__proto__201(余分なフィールドは無視)✅ SAFE
B-02IDOR: 別ユーザーの note を DELETE204ℹ️ 想定どおり(examples に所有権なし)
B-04負 / ゼロの ID404✅ SAFE
B-05整数オーバーフロー ID404✅ SAFE
B-06DELETE してから同じ ID に再アクセス404✅ SAFE
B-07並行 DELETE レースすべて 404(冪等)✅ SAFE
B-081MB 上限のボディ413✅ BLOCKED

フェーズ 6 — API キーバイパス

IDAttackExpectedv1.5.329
A-01キーなし401✅ BLOCKED
A-02クエリ文字列内のキー(?key=?api_key=401✅ BLOCKED
A-03リクエストボディ内のキー401✅ BLOCKED
A-04ヘッダー名のケースバリエーション200(PSR-7 が正規化)✅ SAFE
A-05値の前後の空白200(PSR-7 がトリム)✅ SAFE
A-06//machine/health 二重スラッシュキーなしで 401、ありで 200✅ SAFE
A-07X-Original-URL / X-Rewrite-URL200(ヘッダーは無視)✅ SAFE
A-08OPTIONS プリフライトバイパス405✅ BLOCKED
A-09HEAD メソッド401✅ BLOCKED
A-10一般的なパスワードのブルートフォースすべて 401✅ BLOCKED
A-11URL エンコードされたパス(%6Dachine404✅ BLOCKED
bash
# タイミング攻撃: hash_equals 使用 → 定数時間比較
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: a" http://localhost:8299/machine/health
done)
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: pentest-key" http://localhost:8299/machine/health
done)
# → 10 リクエストでタイミング差 < 5ms: SAFE

フェーズ 7 — インジェクション / XSS / SSTI / コード実行

IDAttackExpectedv1.5.329
I-01XSS <script>alert(1)</script> を保存201、JSON 文字列として返る✅ SAFE — JSON エンコーディングが無効化
I-02SSTI 49 / ${7*7}201、リテラルとして保存✅ SAFE — テンプレートエンジンなし
I-03PHP <?php system("id"); ?>201、リテラルとして保存✅ SAFE
I-04Log4Shell ${jndi:ldap://...}200(ヘッダーは無視)✅ SAFE — Java ではなく PHP
I-051000 階層のネストした JSON400(PHP パース制限)✅ BLOCKED
I-06Unicode BiDi 制御文字201(保存)✅ SAFE — 表示のみのリスク
I-07重複した JSON キー最後の値が勝つ(PHP の振る舞い)ℹ️ INFO-01

格納型 XSS に関する注意: XSS ペイロードは保存され、JSON レスポンスでそのまま返されます。API は JSON 専用(Content-Type: application/json + X-Content-Type-Options: nosniff)であるため、ブラウザはスクリプトを実行しません。リスクが顕在化するのは、別のアプリケーションがこのデータをエスケープせずに HTML コンテキストでレンダリングした場合のみです。

フェーズ 8 — デシリアライゼーション / PHP オブジェクトインジェクション

IDAttackExpectedv1.5.329
D-01パスパラメーター内の phar:// ラッパー404✅ BLOCKED
D-02PHP O:8:"stdClass":... シリアライズペイロード400(無効なボディ)✅ BLOCKED
D-03シリアライズペイロード付き URL エンコードフォーム400(誤った Content-Type)✅ BLOCKED

フェーズ 9 — ヘッダーインジェクション / レスポンス分割

IDAttackExpectedv1.5.329
R-01作成された note id 経由の Location ヘッダーインジェクション/examples/notes/<int>✅ SAFE — int のみ
R-02JWT エラー経由の WWW-Authenticate 内 CRLF無害化された固定メッセージ✅ SAFE
R-03Content-Type スニッフィングX-Content-Type-Options: nosniff✅ SAFE
R-04クリックジャッキングX-Frame-Options: SAMEORIGIN✅ SAFE

フェーズ 10 — CORS / SOP バイパス

IDAttackExpectedv1.5.329
C-01Origin: null(サンドボックス化 iframe)Vary: Origin、ACAO ヘッダーなし✅ SAFE
C-02Origin ヘッダー内の CRLFcurl/http 層で無害化✅ SAFE
C-03Vary ヘッダーキャッシュポイズニングVary: Origin あり✅ SAFE
C-04注入されたメソッド付きプリフライトメソッドは PHP に無視される✅ SAFE
C-05Access-Control-Allow-Origin: *ヘッダーなし(allowlist が空)✅ SAFE

フェーズ 11-20 — エンコーディング / プロトコル / タイミング

IDAttackResult
E-01JSON 内の絵文字 / 高位 Unicode✅ 201(正しく保存)
E-02BiDi RTL オーバーライド(なりすましリスク)✅ 201(表示のみ)
E-05クエリパラメーター経由のページネーション SQLi✅ 422(整数として検証)
H-06bフォールドされた Authorization ヘッダー⚠️ 500(PHP ビルトインのバグ)
20X-Request-Id 129 文字が拒否される✅ サーバーが新しいランダム ID を生成
21X-Request-Id %0a 経由のログインジェクション✅ 拒否(無効な文字)
22Apache ServerTokens/ServerSignatureServer: Apache のみ
23JWT sub=admin 権限昇格✅ クレームは認可に使われない
26JWT リプレイ(2 秒前に期限切れ)✅ 401 Token has expired.
27500 スタックトレース開示✅ 汎用メッセージのみ
28Problem Details instance 内の XSS✅ URL エンコード済み(安全)
29ヘルスチェックエンドポイント経由の SSRF✅ URL は受理されない
15API キータイミングオラクルhash_equals — < 5ms の差

3. 発見事項

VULN-01 — PHP ビルトインサーバーから .htaccess が読める ⚠️ MEDIUM

Trigger: curl http://localhost:8299/.htaccess
Response: 200 + ファイル全体の内容(Apache リライトルール)
Root cause: PHP のビルトインサーバー(php -S)は .htaccess のアクセス制限を強制しません — .htaccess を静的ファイルとして扱います。
Impact: URL リライトルールを露出します。内容は非機密(パスワード/トークンなし)ですが、index.php へのリライトパターンを確認できてしまいます。
Mitigation: セキュリティに敏感なテストでは php -S ではなく Apache コンテナ(docker compose up -d app)を使ってください。Apache は正しく 403 を返します。

bash
# Apache(正しい): 403 Forbidden
curl -si http://localhost:8200/.htaccess | head -1

# PHP ビルトインサーバー(露出): 200 OK
curl -si http://localhost:8299/.htaccess | head -1

VULN-02 — HTTP ヘッダーフォールディングが PHP ビルトインサーバーをクラッシュさせる ⚠️ LOW

Trigger:

GET / HTTP/1.1\r\nHost: localhost\r\nX-NENE2-API-Key:\r\n <key>\r\n\r\n

Response: HTTP/1.0 500 Internal Server Error(空ボディ)
Root cause: PHP のビルトイン HTTP サーバーは RFC 7230 のヘッダーフォールディング(非推奨だが HTTP/1.1 ではまだ有効)をサポートしません。NENE2 フレームワークコードは関与していません。
Impact: 開発時のみ(PHP ビルトインサーバー)。Apache はフォールドされたヘッダーを正しく処理します。

INFO-01 — 重複した JSON キー: 最後の値が勝つ

{"title":"first","title":"INJECTED"}title = "INJECTED"
標準の PHP json_decode の振る舞いです。バリデーションは最終(最後)の値に適用されるため、バリデーションバイパスの経路はありません。認識のために記録します。


4. 検証されたセキュリティ不変条件

これらの保証は 150 件以上のすべてのテストケースで保持されました:

不変条件検証
すべての SQL クエリがパラメーター化されているSLEEP は実行されず、インジェクションペイロードはリテラルとして保存される
JWT は HS256 + 有効な署名 + 整数の exp でなければならない17 件すべての JWT 攻撃バリアントをブロック
API キーは hash_equals でチェックされる10 反復でタイミング差 < 5ms
Content-Length オーバーフローが処理される正しいヘッダーで 413、PHP 警告の漏洩なし
すべてのレスポンスにセキュリティヘッダーCSP / XCTO / XFO / Referrer-Policy / Permissions-Policy を確認
Server: / X-Powered-By: が削除されるApache レスポンスにどちらのヘッダーもなし
スタックトレースが 500 ボディに決して入らない汎用の "The server encountered an unexpected condition." のみ
パストラバーサルがブロックされる15 件すべてのエンコーディングバリアントが 404 を返す
.env / .git / バックアップファイルドキュメントルートですべて 404
CORS デフォルト: 許可オリジンなし任意のオリジンに対して Access-Control-Allow-Origin なし

5. テストスイートの実行

主要チェックの最小限の再現(< 5 分):

bash
TARGET=http://localhost:8299
APIKEY=pentest-key
SECRET=pentest-jwt-secret-32chars-min!!
CID=$(docker ps --filter "publish=8299" --format "{{.ID}}")

# 1. JWT alg:none
H=$(echo -n '{"typ":"JWT","alg":"none"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
P=$(echo -n '{"sub":"admin","exp":9999999999}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
curl -si -H "Authorization: Bearer $H.$P." $TARGET/examples/protected | grep "HTTP/"
# 期待: 401

# 2. SQL インジェクション時間ベース
time curl -so /dev/null -X POST -H "Content-Type: application/json" \
  -d '{"title":"x'\'' AND SLEEP(3)--","body":"x"}' $TARGET/examples/notes
# 期待: 合計 < 500ms

# 3. パストラバーサル
curl -si "$TARGET/%2e%2e/%2e%2e/etc/passwd" | grep "HTTP/"
# 期待: 404

# 4. Content-Length オーバーフロー
curl -si -X POST -H "Content-Length: 9999999999999" $TARGET/ | head -3
# 期待: 413 Request Entity Too Large(200 + PHP 警告ではない)

# 5. API キータイミング
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: a" $TARGET/machine/health
done)
# 期待: 正しいキーと同様のタイミング(hash_equals)

# 6. .htaccess の露出(Apache のみ)
curl -si http://localhost:8200/.htaccess | grep "HTTP/"
# 期待: 403

# 7. JWT exp 必須
NEXP=$(docker exec $CID php -r "
  require 'vendor/autoload.php';
  \$v = new Nene2\Auth\LocalBearerTokenVerifier('$SECRET');
  echo \$v->issue(['sub'=>'user1']);
")
curl -si -H "Authorization: Bearer $NEXP" $TARGET/examples/protected | grep "detail"
# 期待: "Token must contain a numeric exp claim."

関連

MIT ライセンスの下で公開されています。