0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「CORS エラーで画像アップロードが落ちる」と思ったら AWS WAF のデフォルトルールだった話

0
Posted at

「CORS エラーで画像アップロードが落ちる」と思ったら AWS WAF のデフォルトルールだった話

TL;DR

  • 2MB の画像を POST したら、ブラウザに No 'Access-Control-Allow-Origin' header is present + net::ERR_FAILED が出た
  • サーバー側の CORS 設定は何度見直しても正しい
  • 犯人は API Gateway の前に置いていた AWS WAF の AWSManagedRulesCommonRuleSet に含まれる SizeRestrictions_BODY (8KB 超の body を Block)
  • WAF が返す 403 に CORS ヘッダが付かないのでブラウザは「CORS 違反」と表示するが、実体は CORS の話ではなく WAF で弾かれている
  • 解決: WAF のマネージドルールで SizeRestrictions_BODY だけアクションを count に上書きする

起きたこと

個人開発の web アプリ (Next.js + API Gateway + Lambda) で、ユーザーのアバター画像を POST する機能を実装。

ローカルでは動くのに、本番で 2MB の画像をアップロードするとブラウザに:

Access to fetch at 'https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/topic-image'
from origin 'https://www.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Failed to load resource: net::ERR_FAILED
TypeError: Failed to fetch

これまで正常だったもの

  • CORS preflight (OPTIONS): 204 で ACAO ヘッダも付いている
  • 小さいペイロードの POST: 正常動作 (401 だが ACAO 付き)

ペイロードが小さい POST は通るのに、大きい POST だけ CORS エラーになる。CORS 設定が正しいことは curl で検証済。

犯人の特定

curl で 2MB の body を送って生レスポンスを見てみた:

node -e "
const b64 = Buffer.alloc(1500000, 'A').toString('base64');
const body = JSON.stringify({imageBase64: b64, contentType: 'image/jpeg'});
fetch('https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/topic-image', {
  method: 'POST',
  headers: {'Origin': 'https://www.example.com', 'Content-Type': 'application/json'},
  body,
}).then(r => {
  console.log('Status:', r.status);
  console.log('ACAO:', r.headers.get('access-control-allow-origin'));
});"

結果:

Status: 403
ACAO: null
Body: {"message":"Forbidden"}

403 Forbidden でヘッダに ACAO も無い。これは Lambda まで到達していない。API Gateway の手前、つまり WAF がブロックしている


原因: AWSManagedRulesCommonRuleSetSizeRestrictions_BODY

AWS Managed Rule の Common Rule Set に含まれる SizeRestrictions_BODY は、デフォルトで body が 8192 バイトを超えるとブロックする。

画像のような大きな body を想定する API にこのルールを素で当てると即死する。

しかも厄介なのは:

  1. WAF の Block レスポンス (403) には CORS ヘッダが付かない
  2. 結果、ブラウザは「クロスオリジンなのに ACAO が無い = CORS 違反」と判定
  3. フロントエンドの開発者は「自分の CORS 設定を見直そう」と思考が偏る
  4. でもサーバー側の CORS は壊れていない

典型的な「症状と原因が一致しない」トラブル。


解決: SizeRestrictions_BODY を count に上書き

AWS WAFv2 のマネージドルールは、ルール単位でアクションを override できる。CDK で書くと:

const webAcl = new wafv2.CfnWebACL(this, 'ApiWebAcl', {
  scope: 'REGIONAL',
  defaultAction: { allow: {} },
  rules: [
    {
      name: 'AWSManagedCommon',
      priority: 0,
      overrideAction: { none: {} },
      statement: {
        managedRuleGroupStatement: {
          vendorName: 'AWS',
          name: 'AWSManagedRulesCommonRuleSet',
          // ここで特定ルールだけアクションを上書き
          ruleActionOverrides: [
            { name: 'SizeRestrictions_BODY', actionToUse: { count: {} } },
          ],
        },
      },
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: 'AWSManagedCommon',
        sampledRequestsEnabled: true,
      },
    },
  ],
  // ...
});

count に変更すると「ルールが発火したことは CloudWatch に記録するが、Block はしない」状態になる。監視は残しつつ通すという中庸な運用ができる。


なぜ CORS エラーに化けるのかの補足

API Gateway + Lambda Proxy Integration 構成では、Lambda が成功レスポンスに手動で CORS ヘッダを埋め込むのが普通。

return {
  statusCode: 200,
  headers: {
    'Access-Control-Allow-Origin': 'https://www.example.com',
    // ...
  },
  body: JSON.stringify(...),
};

一方、WAF の Block は Lambda より前で発生する。API Gateway の GatewayResponse 側で CORS ヘッダを埋めることも可能だが、デフォルトでは埋まっていない。

よってフロント側は:

  • CORS preflight (OPTIONS) は正常 (Lambda or API Gateway mock integration で CORS 返す)
  • 本番 POST: WAF block → 403 with empty headers → ブラウザ「CORS エラー」

という順序で詰まる。

根本対策の選択肢

  • (採用) WAF のサイズ制限を緩める — 今回はこれで OK、Lambda 側で別途サイズ上限バリデーションあり
  • API Gateway の GatewayResponse で CORS ヘッダを埋める — WAF に弾かれても見た目のエラーがまともになる
  • S3 presigned PUT に切り替える — 大容量のアップロードならこれが正攻法。API Gateway の 10MB 上限も気にしなくてよい

学んだこと

  1. 「CORS エラー」という表示は、本当に CORS の話とは限らない。レスポンスの status code と実際のヘッダを curl で生確認する
  2. WAF / CloudFront / API Gateway など Lambda より前段 の何かがブロックしているとき、エラーはだいたい「CORS エラー」化する
  3. AWS Managed Rule はゼロチューニングで本番投入すると落とし穴に落ちる。Common Rule Set に入っている 20+ のルールをそれぞれ自サービスのユースケースで見直すべき
  4. ルールを削除するのではなく count に落として様子見するのが運用的に安全

参考

LocoCount (https://lococount.com) の実装中に遭遇した話でした。地図上の話題をカウントする web アプリです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?