ByteScope

403

HTTP 403 Forbidden 怎麼修

4xx 用戶端錯誤IETF 標準RFC 9110

伺服器完全看懂了這個請求,而且拒絕執行它——跟 401 不一樣的是,它沒有義務給你任何一個標頭去說明怎樣才行得通。

為什麼會看到 403,以及該怎麼處理

同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。

用 curl 重現 403

對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。

curl
curl -sS -D - -w '\n%{http_code}\n' https://example.com/admin/

這裡刻意沒有把 body 丟掉,因為在 403 上,那往往是唯一寫著原因的地方——§15.5.4 沒有要求任何說明用的標頭,所以那一頁本身就是證據。讀三件事。`server` 標頭和有沒有 `cf-ray`,告訴你這次拒絕是誰寫的:你自己的應用程式、你的代理,還是在兩者之前就回答的邊緣網路。body 告訴你這是哪一種拒絕——你 API 的 JSON 錯誤、nginx 的預設頁,還是一頁帶著規則 id 或 ray id、有品牌樣式的攔截頁。而 `www-authenticate` 不在,本身就是資訊:那個標頭在的話,你面對的其實是 401 的問題、補憑證有可能有用;它不在的話,往這個請求上加憑證不會有任何幫助。想知道是邊緣節點還是來源機拒絕的,同一行加上 `--resolve example.com:443:203.0.113.10` 再跑一次,它會照樣送原本的主機名稱和 SNI,但直接連到你指定的來源位址。403 在直連來源機時消失,就是邊緣或 WAF 規則;還在,就是你自己的伺服器。如果這個拒絕只在瀏覽器上重現得出來,就加 `-H 'user-agent: Mozilla/5.0'`,因為有些規則只看那一個標頭。

403 實際上在講什麼

RFC 9110 §15.5.4 把 403 定義成:伺服器看懂了請求但拒絕滿足它;接著補上那條決定你怎麼查的條款——如果請求有提供認證憑證,伺服器認為那些憑證不足,而且用戶端「不應該」自動用同一組憑證重送。那是一條直接叫你不要重試的指示,也是為什麼把 403 放進重試迴圈永遠是 bug 而不是解法。同一節說,伺服器如果願意公開請求為什麼被禁,可以把原因寫在回應內容裡——注意是「可以」,不是「必須」。403 上沒有任何強制的標頭,而這正是它比 401 難診斷的原因:回應裡沒有任何欄位有義務指出是哪條規則觸發的,所以答案住在某份你得自己去翻的紀錄裡。§15.5.4 最後留了一個逃生口,而這一區大部分的混亂都是它造成的:如果伺服器不想透露這個資源存在,它「可以」改回 404——這代表「一個你明知道存在的東西回 404」和「一個你明知道存在的東西回 403」其實是同一個決定,只是做決定的管理者品味不同。從這裡可以推出兩件實務上的事。第一,403 不在 RFC 9110 §15.1 那份可依啟發式規則快取的狀態碼清單裡,所以中介裝置在沒有明確快取標頭的情況下不會把它存起來——一個在你把權限修好之後還陰魂不散的 403,是某個人寫下的快取政策,不是預設行為。第二,403 跟方法無關、也跟 scheme 無關:它完全沒有在講你認證了沒,這就是為什麼「補上憑證再重試」在這裡是錯的直覺,在 401 上才是對的。

403 最常被搞混的幾個代碼

容易搞混的怎麼分
401401 是「我不知道你是誰」,403 是「我知道,不行」。要讀標頭,不要讀字面:RFC 9110 要求每個 401 都要有 `WWW-Authenticate`,因為換憑證有可能成功;403 什麼都不要求,因為換了也不會成功。重試行為的差別也是從這裡來的——用戶端被期待用新憑證去重試 401,而 §15.5.4 則叫它不要拿同一組憑證去重送 403。
404這兩個常常是同一個決定。§15.5.4 明白允許一台不想透露被禁資源存在的伺服器改回 404,而 AWS 的文件寫著 S3 對沒有 `s3:ListBucket` 的呼叫者做的正是這件事。所以一個在你認證之後變成 200 的 404,從頭到尾就不是路由問題;而一個你無法從別處確認是否存在的路徑回 403,可能還是兩個答案裡比較誠實的那一個。
429限流器不一定要回 429。Cloudflare 的限流規則可以把動作設成直接封鎖而不是回 429,而 GitHub 的文件也寫著超過它的速率限制時可能回 403 也可能回 429。所以一個只在高負載時出現、等一下就自己好的 403,是限流不是權限——在你跑去稽核角色設定之前,先看它跟請求量有沒有相關。
451451(RFC 7725,Unavailable For Legal Reasons)是更精確的代碼,用在「因法律要求而拒絕」而不是「營運者自己的政策」上,而且它要求回應帶一個 `rel="blocked-by"` 的 `Link` 標頭,指出提出要求的那一方。一個網站對法律封鎖的資源回 403 並沒有錯,只是比較不精確——但如果你看到的是 451,那個問題永遠不是技術能解的。
nginxnginx 會從三個彼此無關的地方產生 403,而錯誤紀錄一行就分得出來。存取模組裡的 `deny` 是照用戶端位址拒絕;`open() ... failed (13: Permission denied)` 是檔案系統拒絕了 worker 使用者;而 `directory index of "..." is forbidden` 代表有人要了一個目錄、裡面沒有索引檔、而 `autoindex` 是關的。在這三個之間用猜的,代價遠比讀一行紀錄高。
原因片語
Forbidden
類別
4xx 用戶端錯誤
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

這些全都在你的瀏覽器裡跑,不會上傳任何東西。

相關狀態碼

查 403 的時候,通常也會順手看一下這幾個。

常見問題

怎麼知道這個狀態碼是哪一台伺服器回的?

去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 serverviacf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --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 可能會刷兩次卡。對一台已經在失敗的伺服器丟出重試風暴,是短暫事故變成長時間事故最常見的那條路。

我可以讓自己的伺服器改回別的狀態碼嗎?

可以,唯一要守的規則是那個代碼必須是真的。所有會自動讀你回應的東西——搜尋引擎、監控、快取、用戶端的重試邏輯——都只看那個數字,從來不看那一頁寫了什麼。用 200 回一頁錯誤訊息,會把故障從你自己的警報裡藏起來;用 200 回一頁不存在的內容,會讓錯誤被當成內容收進索引;而對一個只是格式不對的請求回 500,會把值班的人送去堆疊裡錯的那一半。

403 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。