👾

Playwright + OWASP ZAP + Claude Code で E2E テストから脆弱性診断まで一気通貫でできるかやってみた

に公開

はじめに

最近、他チームのエンジニアが「Playwright で書いた E2E テストを OWASP ZAP に通して脆弱性診断をやってみた」という話を聞きました。

「E2E のシナリオってそのまま脆弱性診断にも使えるの?」

気になったので自分でも試してみました。その記録です。

やったこと(全体像)

シンプルな Todo アプリを題材に、以下の流れで試しました。

1. Next.js で Todo アプリを構築
2. Playwright で E2E テスト(12シナリオ)を作成
3. バックエンドを FastAPI + MySQL に変更(Docker)
4. E2E シナリオを ZAP プロキシ経由で実行 → 脆弱性診断

ポイントは ステップ 4。E2E テストのシナリオをそのまま脆弱性スキャンに流用できるかの実験です。

Playwright で E2E テストを導入する

セットアップ

npm install -D @playwright/test
npx playwright install chromium

設定は playwright.config.ts に書きます。開発サーバーの自動起動もやってくれるので楽です。

export default defineConfig({
  testDir: "./e2e",
  use: { baseURL: "http://localhost:3000" },
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});

テストの雰囲気

Playwright のテストはかなり読みやすいです。

test("入力して追加ボタンでTodoが追加される", async ({ page }) => {
  await page.getByLabel("新しいTodoを追加").fill("牛乳を買う");
  await page.getByRole("button", { name: "追加" }).click();
  await expect(page.getByText("牛乳を買う")).toBeVisible();
});

こういうのを 12 ケース書きました(追加・完了トグル・削除・フィルター・一括削除・永続化)。

ハマったポイント

いくつか地味にハマったので共有します。

セレクタの曖昧さ: getByRole("button", { name: /未完了/ }) が、フィルターボタンと Todo の「未完了にする」ボタンの両方にマッチして Playwright が怒る。→ /^未完了\(/ のように正規表現で絞り込んで解決。

テキストの部分一致: getByText("完了タスク") が「未完了タスク」にもマッチ。→ { exact: true } を付けるか、テストデータ名を被らないようにする。

disabled ボタン: 空文字テストで追加ボタンが disabled → クリックがタイムアウト。→ Enter キー送信に変更。

どれも「あるある」ですが、知っていると時間を節約できます。

バックエンドを API 化する(FastAPI + MySQL)

脆弱性診断をするには HTTP 通信が必要なので、localStorage から API バックエンドに切り替えました。

# docker-compose.yml
services:
  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
  backend:
    build: ./backend
    depends_on:
      db: { condition: service_healthy }

FastAPI 側は 6 エンドポイント。SQLAlchemy ORM + Pydantic スキーマで構成しています。

E2E テスト側の変更は、beforeEachlocalStorage.clear()API 全件削除 に変えたのと、直列実行にしたくらいです。テスト自体はほぼそのまま動きました。

本題:E2E シナリオで OWASP ZAP の脆弱性診断

やり方はシンプル

ZAP を Docker で起動して、Playwright テストをプロキシ経由で流すだけ。

# ZAP をプロキシとして起動
docker run --rm -d --name zap \
  --network e2e-test_default -p 8080:8080 \
  ghcr.io/zaproxy/zaproxy:stable \
  zap.sh -daemon -host 0.0.0.0 -port 8080 \
  -config 'api.disablekey=true'

# Playwright テストを ZAP 経由で実行
HTTP_PROXY=http://localhost:8080 npx playwright test

環境変数を 1 つ足すだけで、E2E テストのトラフィックが全部 ZAP を通ります。テスト自体は普通に全 12 件パスしました。

その後、ZAP のアクティブスキャンを実行。

# アクティブスキャン開始
curl "http://localhost:8080/JSON/ascan/action/scan/?url=http://backend:8000&recurse=true"

# レポート取得
curl "http://localhost:8080/OTHER/core/other/htmlreport/" > zap-report.html

実際に検出された脆弱性

リスク 件数 主な内容
High 1 認証・認可なし(全 API が無認証)
Medium 4 CSP 未設定、クリックジャッキング対策なし、SRI なし、Stored XSS 可能性
Low 3 X-Content-Type-Options なし、外部 JS 読み込み、サーバー情報露出

一方で、SQL インジェクションは安全でした。SQLAlchemy の ORM がパラメータバインディングを使っているため、' OR 1=1 -- のような入力もただの文字列として DB に保存されるだけ。Pydantic による Mass Assignment 対策も効いていて、不正なフィールド(idcompleted)を送っても無視されます。

手動テストも併用

ZAP だけでなく、curl で直接攻撃パターンを送るテストも実施しました。

# SQL Injection テスト → 安全(ただの文字列として保存される)
curl -X POST http://localhost:8000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "test'\'' OR 1=1 --"}'

# XSS テスト → DB に保存されるが、React が自動エスケープ
curl -X POST http://localhost:8000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "<script>alert(1)</script>"}'

# CORS テスト → 不正オリジンはブロック
curl -I http://localhost:8000/api/todos -H "Origin: http://evil.com"
# → HTTP 400 Bad Request ✅

Claude Code との組み合わせが便利だった

今回の検証は Claude Code を使いながら進めたのですが、これがかなり楽でした。

「Todo アプリ作って」→ アプリ完成
「E2E テスト書いて」→ 12 シナリオ完成
「バックエンド MySQL にして」→ API + Docker 構成完成
「脆弱性診断して」→ ZAP 実行 + レポート作成

特に E2E テストは、機能を追加・変更したときに「テストも直して」と言えばソースコードとテストコードを同時に更新してくれるので、メンテナンスが楽です。

ZAP も万能ではない

正直に書くと、ZAP だけでは検出できないものもあります。

  • ビジネスロジックの脆弱性(例:他のユーザーの Todo を操作できるか)は検出不可
  • 誤検知もある(Swagger UI の CDN 読み込みなど、開発環境限定の指摘)
  • ZAP のレポートを読んで「対応すべきか」の判断にはセキュリティの基礎知識が必要

あくまで一次スクリーニングとして使うのがよさそうです。「明らかにまずいもの」を開発中に早期発見できるだけでも十分価値があると感じました。

今回の検証の前提と留意点

今回は Todo アプリという非常にシンプルな題材で試しただけなので、実際のプロダクトに適用する際には他にも課題が出てくると思います。思いつくところだと:

  • 認証が絡むアプリ: 今回のアプリには認証がなかったので、ログインが必要なアプリで ZAP を通す場合はセッション管理やトークンの扱いが別途必要になるはず
  • SPA のルーティング: 今回は 1 ページ構成だったけど、複数ページや動的ルーティングがあると ZAP の Spider がうまく巡回できないケースがあるかも
  • CI での実行時間: 12 シナリオ + ZAP のアクティブスキャンで数分程度だったけど、シナリオが増えたときにどこまでスケールするかは未検証
  • ZAP のチューニング: デフォルト設定で動かしただけなので、スキャンポリシーのカスタマイズや誤検知の抑制設定は試せていない

あくまで「こういうことができそう」という感触を得た段階で、本番運用に向けてはもう少し検証が必要そうです。

まとめ

検証項目 結果
Playwright で E2E テストが実用的に運用できるか
E2E シナリオを ZAP の脆弱性スキャンに流用できるか
ZAP が実際にセキュリティ指摘を検出できるか
全部 OSS で完結するか

E2E テストを書く → 環境変数 1 つ足すだけで脆弱性診断もできる。思った以上にお手軽でした。

E2E テストを導入済みのプロジェクトなら、ZAP を足すだけで脆弱性の一次チェックが追加できるので、試してみる価値はあると思います。

参考になれば幸いです!

レスキューナウテックブログ

Discussion