403
サーバーはリクエストの意味を完全に理解したうえで、実行を拒んでいます。しかも 401 と違って、何を出せば通ったのかを説明するヘッダーを添える義務はありません。
同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。
これが出ている理由
明らかにサインイン済みのリクエストに 403 が返るときの原因は数えるほどしかなく、しかもたいてい `fetch` の呼び出しの中にはありません。いちばん多いのは CSRF トークンです。無い、古い、あるいはその後ローテーションされたセッションに紐づいている——Django の `CsrfViewMiddleware` はまさにこの場合に 401 ではなく 403 を返すので、一晩開きっぱなしにしたページは、GET は全部通るのに最初の POST だけが落ちるようになります。2 つ目は期限を過ぎた署名付き URL です。署名の期限が切れた瞬間、オブジェクトストレージは access-denied のボディを付けて 403 を返すので、キャッシュして使い回していた URL がしばらくは動いて、ある時点から動かなくなります。3 つ目はそもそもあなたの API ではありません。WAF がそのリクエストを攻撃と判断して自前の HTML ブロックページを返しており、`res.json()` は最初からあなたのものではない文書の上で例外を投げています。
やること
何より先に、受け取ったのが誰のページなのかを突き止めてください。ボディが API の JSON ではなく HTML なら、`server` を読み、`cf-ray` を探します。ブランドの付いたブロックページと ray id はエッジのルールで、その ray id はサポートが直接引ける文字列です。CSRF なら、古いトークンを再送するのではなくページを読み込み直して新しいトークンを取ってください。「ハードリロードすると通る」は診断結果であって修正ではありません。署名付き URL は保存せず、使う直前に取り直します。そして再試行を実装しないこと。§15.5.4 は同じ認証情報でリクエストを繰り返さないようクライアントに求めているので、403 に対する再試行ループは、WAF がすでに嫌っているトラフィックを増やすだけです。
これが出ている理由
自分の認可層が拒んだか、その手前の何かが拒んだかのどちらかで、たいていエラーログの 1 行がどちらかを名指しします。nginx には外から見ると見分けのつかない 403 が 3 種類あります。access モジュールの `deny` ディレクティブはアドレスで拒否します。`open() "/path" failed (13: Permission denied)` はファイルシステムがワーカーのユーザーを拒んだもので、nginx がそれを 403 に写します。`autoindex off` のディレクトリへのリクエストは `directory index of "/path/" is forbidden` を記録します。返せる index ファイルが無く、一覧表示も無効だからです。オブジェクトストレージの罠はまた別で、しかも意図的です。AWS は、`s3:ListBucket` を持たない呼び出し元には存在しないキーに対しても 404 ではなく 403 Access Denied を返すと明記しています。応答をキーの存在確認に使えないようにするためです。
やること
アプリケーションのコードに触る前にプロキシのエラーログを読んでください。先ほどの nginx の 3 つは修正方法が 3 つとも違い、ログの 1 行がそれを無料で分けてくれます。次に、そのリクエストがそもそもアプリケーションまで届いたのかを確定します。自分のログで自分のリクエスト ID を探し、まったく現れないなら拒否は上流で起きていて、自分の認可コードは一切関係ありません。ファイルシステムの場合は、ファイルのモードと所有者を確認したうえで、親ディレクトリすべての実行ビットを見てください。途中のディレクトリを辿れないワーカーは、ファイルを読めないワーカーとまったく同じ 403 を出します。オブジェクトストレージでは、デバッグ中に 404 を区別したいならテスト用ポリシーで `s3:ListBucket` を付与し、本番ではその曖昧さこそが機能なのだと覚えておいてください。WAF なら、ブロックページのルール ID を見つけて、パスではなくリクエストボディと突き合わせます。
これが出ている理由
サイトはあなたが何を求めたかを分かったうえで、それは渡せないと判断しています。リンク切れとも、パスワードの問題とも違います。これは拒否であり、たいていは入力した内容ではなく、どのアカウントか・どの場所からか・どのネットワークからか、の話です。地域や有料プランに限定されたコンテンツ、あるアドレス宛てに権限が付与された共有ドキュメントを別のアドレスで開いた場合、期限切れの招待リンク、そしてサイトがブロックしている VPN や公衆ネットワーク——どれも同じページになります。
やること
まずどのアカウントでサインインしているかを確認してください。仕事用アドレスに共有されたリンクを、個人アカウントでサインインしたブラウザで開く——これが群を抜いて多い原因で、10 秒で潰せます。VPN や社内ネットワーク、公衆 Wi-Fi を使っているなら、外して試してください。アドレスの帯ごと拒否しているサイトがあります。ページに参照 ID や ray id が書かれていたら、その文字列を控えておいてください。サイトの運営者が直接引ける唯一の手がかりです。リンクが他人から来たものなら、手元のものを繰り返し試すより、いま使っているアドレス宛てに共有し直してもらうほうが早いです。
失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。
curl -sS -D - -w '\n%{http_code}\n' https://example.com/admin/ここでは意図的にボディを捨てていません。403 では、理由が書かれている場所がそこしかないことがよくあるからです。§15.5.4 は説明のためのヘッダーを一切要求していないので、証拠はページそのものです。読むべきは 3 つ。`server` ヘッダーと `cf-ray` の有無は、拒否を書いたのが誰かを教えます——自分のアプリケーションか、自分のプロキシか、その手前で答えたエッジのネットワークか。ボディはどの種類の拒否かを教えます——自分の API の JSON エラーか、nginx の既定ページか、ルール ID や ray id を載せたブランド付きのブロックページか。そして `www-authenticate` が無いこと自体が情報です。あれば見ているのは 401 の問題で認証情報が効くかもしれず、無ければこのリクエストに認証情報を足しても意味がありません。拒否したのがエッジかオリジンかを知るには、同じコマンドを `--resolve example.com:443:203.0.113.10` 付きで実行します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ curl が接続します。オリジンに直接当てると消える 403 はエッジか WAF のルール、残る 403 は自分のサーバーです。ブラウザからしか再現しないなら `-H 'user-agent: Mozilla/5.0'` を足してください。そのヘッダーだけを見て発火するルールがあります。
RFC 9110 §15.5.4 は 403 を、サーバーがリクエストを理解したうえで実行を拒む状態と定義し、デバッグの進め方を決める一文を添えています。認証情報が提示されていた場合、サーバーはそれを不十分とみなしており、クライアントは同じ認証情報でリクエストを自動的に繰り返すべきではない (SHOULD NOT)、というものです。これは「再試行するな」という直接の指示で、再試行ループの中の 403 が回避策ではなく常にバグである理由もここにあります。同じ節は、なぜ禁止されたのかを公にしたいサーバーは応答の内容でその理由を説明できる、とも書いています。「できる」であって「しなければならない」ではないところに注目してください。403 に必須のヘッダーは一つもありません。まさにこれが、401 より診断しにくい理由です。どのルールが発火したのかを応答のどこかが名乗る義務は無いので、答えは自分で読みに行くログの中にしかありません。§15.5.4 はこの分野の混乱の大半を生む抜け道で締めくくられています。リソースの存在を明かしたくないサーバーは代わりに 404 を送ってもよい (MAY)。つまり、あると分かっているものへの 404 と、あると分かっているものへの 403 は、趣味の違う管理者が下した同じ判断です。ここから運用上の事実が 2 つ出てきます。1 つ目、403 は RFC 9110 §15.1 のヒューリスティックにキャッシュ可能なステータスコードの一覧に入っていないので、明示的なキャッシュヘッダーが無ければ中間装置は保存しません。権限を直したあとも残る 403 は既定の挙動ではなく、誰かが書いたキャッシュのポリシーです。2 つ目、403 はメソッドにもスキームにも無関心で、認証したかどうかについては何も語りません。「認証情報を足して再試行」がここでは間違った勘であり、401 では正しい勘である理由がこれです。
| 紛らわしいコード | 見分け方 |
|---|---|
| 401 | 401 は「あなたが誰か分からない」、403 は「分かったうえで、駄目だ」です。言葉ではなくヘッダーを読んでください。RFC 9110 がすべての 401 に `WWW-Authenticate` を要求するのは、違う認証情報なら成功しうるからで、403 に何も要求しないのは成功しえないからです。再試行の扱いが違うのも同じ理由です。401 はクライアントが新しい認証情報で再挑戦することを想定していますが、§15.5.4 は 403 を同じ認証情報で繰り返さないよう求めています。 |
| 404 | この 2 つはしばしば同じ判断です。§15.5.4 は、禁止されたリソースの存在を明かしたくないサーバーが代わりに 404 で答えることを明示的に許していますし、AWS は `s3:ListBucket` を持たない呼び出し元に対して S3 がまさにそう振る舞うと明記しています。だから認証したとたん 200 に変わる 404 は、最初からルーティングの問題ではありませんでしたし、存在を他の手段で確認できないパスへの 403 は、2 つのうち正直なほうかもしれません。 |
| 429 | レート制限が必ず 429 で答えるとは限りません。Cloudflare のレート制限ルールは、アクションを 429 ではなくブロックに設定できますし、GitHub はレート制限の超過が 403 でも 429 でも返りうると明記しています。だから負荷が高いときだけ現れ、しばらく待つと自然に消える 403 は、権限ではなく制限です。ロールの棚卸しに向かう前に、リクエスト量と相関しているかを確かめてください。 |
451 | 451 (RFC 7725, Unavailable For Legal Reasons) は、運営者自身のポリシーではなく法的な要求によって拒否した場合の、より狭いコードです。要求した主体を示す `rel="blocked-by"` 付きの `Link` ヘッダーを応答に載せることを求めています。法的にブロックされたリソースに 403 を返すサイトが間違っているわけではなく、単に具体性が低いだけです。ただし 451 を見たなら、その解決は技術の側にはありません。 |
nginx | nginx は互いに無関係な 3 か所から 403 を生み、エラーログの 1 行がそれを分けます。access モジュールの `deny` はクライアントのアドレスで拒否、`open() ... failed (13: Permission denied)` はファイルシステムがワーカーのユーザーを拒否、`directory index of "..." is forbidden` はディレクトリが要求されたが index ファイルが無く `autoindex` も off、という意味です。この 3 つを当てずっぽうで区別するコストは、ログを読むコストよりずっと高くつきます。 |
どれもブラウザ内で完結します。アップロードは発生しません。
403 を調べているとき、ついでに読むことになりがちなコードです。
ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、server・via・cf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。
ありません。分岐条件にしてはいけません。RFC 9112 はクライアントに理由句を無視するよう求めていますし、サーバーは自由に変えられますし、HTTP/2 と HTTP/3 にはそもそも理由句が存在しません。HTTP/1.1 では 404 Not Found として届くものが、HTTP/2 では素の 404 として届きます。このサイトが登録済みの理由句を載せているのは、それが検索される語であり HTTP/1.1 のログに出る文字列だからで、何かのソフトウェアがそれに依存しているからではありません。
5xx と 429 は再試行してよく、4xx はしないでください。次に投げても中身は同じだからです。応答に Retry-After が付いていればそれに従ってください。RFC 9110 がまさにこのために定義していて、値は秒数でも日時でも構いません。付いていなければ、ジッター付きの指数バックオフと明確な上限を使い、しかも冪等なメソッドに限ってください。再試行された POST はカードに二重で課金しかねません。すでに失敗しているサーバーへの再試行の嵐は、短い障害を長い障害に変える典型的な経路です。
変えられます。守るべき規則は 1 つだけ、そのコードが本当であること。応答を自動で読むもの——検索エンジン、監視、キャッシュ、クライアント側の再試行ロジック——はすべて数字だけで判断し、ページの中身は一切見ません。エラーページを 200 で返せば自分の監視から障害が隠れますし、無いページを 200 で返せばエラーが本文として索引されますし、単に不正なリクエストに 500 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。
403 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。