Skip to content

本番環境でCORSエラーが発生 - WAFのpreflight OPTIONS処理を修正 #30

@takaokouji

Description

@takaokouji

問題の概要

Chromebookから本番環境のmeshV2 (https://graphql.api.smalruby.app/graphql) に接続すると、CORSエラーが発生します。

エラーメッセージ

Access to fetch at 'https://graphql.api.smalruby.app/graphql' from origin 'https://smalruby.app'
has been blocked by CORS policy: Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

環境情報

  • Origin: https://smalruby.app (正しい)
  • リクエスト先: https://graphql.api.smalruby.app/graphql (正しい)
  • 環境: 本番環境 (prod)
  • デバイス: Chromebook

原因分析

1. WAF設定の問題

infra/mesh-v2/lib/mesh-v2-stack.ts:139-192 のWAF設定を確認したところ、以下の問題が見つかりました:

現在の設定:

const webAcl = new wafv2.CfnWebACL(this, 'MeshV2ApiWebAcl', {
  defaultAction: { block: {} },  // デフォルトでブロック
  scope: 'REGIONAL',
  rules: [
    {
      name: 'AllowSpecificOrigins',
      priority: 1,
      action: { allow: {} },
      statement: {
        orStatement: {
          statements: allowedOrigins.map(origin => ({
            byteMatchStatement: {
              fieldToMatch: {
                singleHeader: { name: 'origin' }  // Originヘッダーのみチェック
              },
              positionalConstraint: 'EXACTLY',
              searchString: origin,
              // ...
            },
          })),
        },
      },
      // ...
    },
  ],
});

問題点:

  • デフォルトアクションが block のため、許可ルールに一致しないリクエストはすべてブロックされる
  • preflight OPTIONSリクエストが正しく処理されていない可能性がある
  • Originヘッダーは正しくチェックされているが、HTTPメソッド (OPTIONS) の特別な扱いがない

2. AppSyncのCORS動作

AppSync GraphQL APIは通常、以下のCORSヘッダーを自動的に返します:

  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Headers: Content-Type, Authorization, x-api-key, ...
  • Access-Control-Allow-Methods: POST, OPTIONS

しかし、カスタムドメイン + WAFの組み合わせでは、以下のいずれかの問題が発生する可能性があります:

  1. WAFがpreflight OPTIONSリクエストをブロックしている
  2. AppSyncがレスポンスにCORSヘッダーを追加していない
  3. WAFがレスポンスからCORSヘッダーを削除している

3. preflight OPTIONSリクエストの流れ

  1. ブラウザが OPTIONS リクエストを送信 (preflight)

    • Header: Origin: https://smalruby.app
    • Header: Access-Control-Request-Method: POST
    • Header: Access-Control-Request-Headers: content-type, x-api-key
  2. WAFが Originヘッダーをチェック → https://smalruby.app なので許可

  3. AppSyncがOPTIONSリクエストを処理

  4. 問題: レスポンスに Access-Control-Allow-Origin ヘッダーがない

    • WAFがヘッダーを削除している可能性
    • AppSyncがヘッダーを返していない可能性

解決策の提案

解決策1: WAFルールにOPTIONSメソッドの特別なルールを追加 (推奨)

preflight OPTIONSリクエストを常に許可し、AppSyncがCORSヘッダーを返せるようにする。

変更内容:

rules: [
  {
    name: 'AllowPreflightOptions',
    priority: 0,  // 最優先
    action: { allow: {} },
    statement: {
      andStatement: {
        statements: [
          {
            byteMatchStatement: {
              fieldToMatch: {
                method: {}  // HTTPメソッドをチェック
              },
              positionalConstraint: 'EXACTLY',
              searchString: 'OPTIONS',
              textTransformations: [{ priority: 0, type: 'UPPERCASE' }]
            }
          },
          {
            orStatement: {
              statements: allowedOrigins.map(origin => ({
                byteMatchStatement: {
                  fieldToMatch: { singleHeader: { name: 'origin' } },
                  positionalConstraint: 'EXACTLY',
                  searchString: origin,
                  textTransformations: [{ priority: 0, type: 'LOWERCASE' }]
                }
              }))
            }
          }
        ]
      }
    },
    visibilityConfig: {
      cloudWatchMetricsEnabled: true,
      metricName: 'AllowPreflightOptions',
      sampledRequestsEnabled: true
    }
  },
  {
    name: 'AllowSpecificOrigins',
    priority: 1,
    // ... (既存のルール)
  }
]

メリット:

  • preflight OPTIONSリクエストが確実に許可される
  • AppSyncがCORSヘッダーを返せるようになる
  • 他のOriginからのOPTIONSリクエストはブロックされる (セキュリティ保持)

解決策2: CloudFrontディストリビューションを追加

AppSyncの前段にCloudFrontを配置し、CORSヘッダーを追加する。

メリット:

  • より柔軟なCORS設定が可能
  • キャッシュによるパフォーマンス向上
  • Lambda@Edgeでヘッダーをカスタマイズできる

デメリット:

  • 設定が複雑
  • コスト増加
  • WebSocketサポートに注意が必要

解決策3: AppSyncのデフォルトエンドポイントを使用

カスタムドメインを使わず、AppSyncのデフォルトエンドポイントを使用する。

メリット:

  • AppSyncがCORSヘッダーを自動的に返す
  • 設定不要

デメリット:

  • カスタムドメインが使えない
  • URLが長くなる
  • ブランディングが弱くなる

推奨アクション

解決策1 (WAFルールの追加) を推奨します。理由:

  1. 最小限の変更で解決できる
  2. セキュリティを保ちながらCORSを有効化できる
  3. カスタムドメインを維持できる
  4. 追加コストなし

実装手順

  1. infra/mesh-v2/lib/mesh-v2-stack.ts を更新してOPTIONSルールを追加
  2. npx cdk diff --context stage=prod で変更を確認
  3. npx cdk deploy --context stage=prod でデプロイ
  4. Chromebookでテストして動作確認
  5. CloudWatch Logsで WAFメトリクスを確認

検証方法

デプロイ後、以下の方法で検証:

1. curlでpreflight OPTIONSリクエストをテスト

curl -X OPTIONS https://graphql.api.smalruby.app/graphql \
  -H "Origin: https://smalruby.app" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, x-api-key" \
  -v

期待される結果:

< HTTP/2 200
< access-control-allow-origin: https://smalruby.app
< access-control-allow-methods: POST, OPTIONS
< access-control-allow-headers: content-type, x-api-key

2. ブラウザのDevToolsで確認

  1. Chromebookで https://smalruby.app を開く
  2. DevTools → Network タブ
  3. meshV2に接続
  4. OPTIONSリクエストのレスポンスヘッダーを確認

3. WAFメトリクスの確認

aws cloudwatch get-metric-statistics \
  --namespace AWS/WAFV2 \
  --metric-name AllowedRequests \
  --dimensions Name=Rule,Value=AllowPreflightOptions \
  --start-time 2025-01-18T00:00:00Z \
  --end-time 2025-01-18T23:59:59Z \
  --period 3600 \
  --statistics Sum

参考情報

追加調査

もし解決策1で解決しない場合、以下を確認:

  1. Route53のAレコード設定が正しいか
  2. ACM証明書が正しく設定されているか
  3. AppSyncのカスタムドメイン設定が正しいか
  4. WAFログでリクエストがどのルールで処理されているか確認

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions