ByteScope

403

HTTP 403 Forbidden の直し方

4xx クライアントエラーIETF 標準RFC 9110

サーバーはリクエストの意味を完全に理解したうえで、実行を拒んでいます。しかも 401 と違って、何を出せば通ったのかを説明するヘッダーを添える義務はありません。

403 が出ている理由と、やるべきこと

同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。

curl で 403 を再現する

失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。

curl
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'` を足してください。そのヘッダーだけを見て発火するルールがあります。

403 が実際に意味していること

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 では正しい勘である理由がこれです。

403 と間違えられやすいコード

紛らわしいコード見分け方
401401 は「あなたが誰か分からない」、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 は、権限ではなく制限です。ロールの棚卸しに向かう前に、リクエスト量と相関しているかを確かめてください。
451451 (RFC 7725, Unavailable For Legal Reasons) は、運営者自身のポリシーではなく法的な要求によって拒否した場合の、より狭いコードです。要求した主体を示す `rel="blocked-by"` 付きの `Link` ヘッダーを応答に載せることを求めています。法的にブロックされたリソースに 403 を返すサイトが間違っているわけではなく、単に具体性が低いだけです。ただし 451 を見たなら、その解決は技術の側にはありません。
nginxnginx は互いに無関係な 3 か所から 403 を生み、エラーログの 1 行がそれを分けます。access モジュールの `deny` はクライアントのアドレスで拒否、`open() ... failed (13: Permission denied)` はファイルシステムがワーカーのユーザーを拒否、`directory index of "..." is forbidden` はディレクトリが要求されたが index ファイルが無く `autoindex` も off、という意味です。この 3 つを当てずっぽうで区別するコストは、ログを読むコストよりずっと高くつきます。
理由句
Forbidden
クラス
4xx クライアントエラー
定義
RFC 9110
標準
IETF 標準

このサイトのツール

どれもブラウザ内で完結します。アップロードは発生しません。

関連するステータスコード

403 を調べているとき、ついでに読むことになりがちなコードです。

よくある質問

このステータスコードを出したのがどのサーバーかを知るには?

ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、serverviacf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。

数字のあとの理由句(Not Found など)に意味はありますか?

ありません。分岐条件にしてはいけません。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 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。