跳至主要內容
>_ITDITD網站資安平台

資安指南

X-Forwarded-For(XFF)偽造是什麼 — 信任代理設定的陷阱與防禦

X-Forwarded-For(XFF)是用戶端可以隨意偽造的 HTTP 標頭。本文以個人開發中常見的事例,從防禦視角講解:在偽造的 XFF 裡夾帶 SQL 注入或 XSS 探測的自動掃描,以及『信任所有代理』這一設定的陷阱(不提供攻擊步驟)。多層防禦為什麼有效、即便如此還有什麼本該修復,將分成對症處理與根本對策來總結。

發布於 2026-06-08 閱讀時間 4 分鐘

X-Forwarded-For(XFF)是即便經過代理,也用來告訴應用程式『真正的訪客 IP 是這個』的標頭。然而這個值用戶端可以隨意偽造。本文把個人開發中常見的『遭到 XFF 偽造掃描』事例隱去專有名詞,轉化為教訓(不提供攻擊的重現步驟)。

事例摘要 — CASE FILE
種類
X-Forwarded-For 偽造 + 注入探測(自動掃描)
實際損害
無(被多層防禦擋下)
發現方式
攻擊請求變成 500,異常通知郵件大量飛來
生效的盾
①佔位符(值的繫結)②DB 的字元編碼驗證
殘留的疏漏
『信任所有代理』的設定(前端並無代理的架構)
處置
在邊界對 IP 標頭做淨化(對症)→ 把信任代理收斂到合適範圍(根本)
0
實際損害(外洩・竄改)
2 道
生效的盾(多層防禦)
全部允許
代理信任的設定
邊界驗證
真正的對策

先做區分:是『攻擊』還是『自己的失誤』

看到錯誤通知就立刻斷定『是攻擊』,是要避免的鐵律。如果剛剛做過架構變更,就應以同等分量去懷疑自己的改動。

1

懷疑自己的改動

先立一個假設:最近的設定變更導致使用者反覆重新登入。但這無法解釋混入保存值裡的非法位元組序列 → 否決。
2

用值的內容作為證據

混入的是引號的多重 URL 編碼、以及 overlong(冗餘)位元組序列。這不是正常使用者或代理會產生的值 → 斷定為惡意酬載
3

是定向攻擊還是掃描

與其說是針對特定目標,更可能是在整個網際網路上無差別掃蕩的自動掃描器,先這樣判斷。

本站的視角:從症狀反推原因時要『用資料來排除』

『最近動過的地方可疑』這種直覺,作為出發點是有效的。但是,如果證據(這裡是值的內容)予以否定,就要放手。立多個假設,不靠成見而用資料一個個排除,誤診就會大幅減少。

攻擊的真面目:在 XFF 偽造裡搭載注入探測

攻擊機器人在 XFF 裡『以 IP 之名』塞入探測用字串,只在末尾留一段像樣的 IP。目的只有一個——

『這個應用程式,會不會相信 XFF 的值,把它生拼進 DB 查詢、或原樣輸出到 HTML 裡?』

如果應用程式做得馬虎,就會經由 XFF 中招 SQL 注入XSS。這是把『承載 IP 的位置』濫用為任意字串的注入口的手法。

為什麼實際損害為零:兩道盾

盾① 值的繫結(佔位符)

沒有把 IP 值做字串拼接,而是用佔位符把它作為資料交給 DB。輸入沒有變成 SQL『指令』的餘地 → SQLi 在結構上不成立。這是使用框架最大的好處之一。

盾② DB 的字元編碼驗證

酬載裡的非法位元組(overlong UTF-8 等),在欄的字元編碼下本就是無法保存的排列。DB 判定為『無效』,拒絕寫入並拋出例外 → 攻擊失敗,而且被偵測到了

實際發生的只是『攻擊者自己的請求變成 500,異常通知大量飛來』而已。外洩、竄改、非法登入一概沒有。這是多層防禦按預期生效的好例子。

真正的問題:把代理『全部』都信任了

如果到此為止那就是佳話,但有一點不能放過。話說回來,偽造的 XFF 為什麼能抵達 IP 判定?

✗ 信任全部代理(萬用字元)

用戶端偽造的 XFF → 被當作『真實的 IP』採用 → 記錄・工作階段・IP 判定被汙染

✓ 什麼都不信任(預設值)

忽略偽造的 XFF → 採用直接連線來源的真實 IP → 偽造失效

把信任代理設成萬用字元,誰偽造的 XFF 都會被當成『真實的 IP』通過。前端沒有代理時,『什麼都不信任』才是正解。

這個架構是既無反向代理也無 CDN、直連的 Web 伺服器。也就是說根本沒必要信任 XFF,卻設成了『全部信任』。『先全部允許再說』,正是開發時的偷懶原封不動殘留到生產環境的典型。

盲點:XFF 偽造的危害不只有『注入』

所有相信用戶端 IP 的處理都會被汙染。rate limit 的繞過、IP allowlist 的突破、BAN 規避、稽核記錄的偽造——即便注入防禦很牢固,這裡也是另一回事。請把『相信 IP 的地方』盤點出來,確認其信任的依據(=proxy 設定)。

對策:在邊界淨化 → 把信任收斂到合適範圍

1

在邊界對 IP 標頭做淨化(對症・速效)

在 proxy 信任處理執行之前,把 XFF 切分,對每個元素以『是否為合法 IP』來驗證,只保留合法的 IP。一個都沒有就連標頭一起刪除。正常使用者的真實 IP 能通過驗證所以不受影響,探測字串會被丟棄。
2

把信任代理收斂到合適範圍(根本)

棄用萬用字元。前端沒有代理就什麼都不信任。proxy 信任還關係到 http/https 判定,改動可能帶來重新導向迴圈等副作用 → 先在一個環境裡先行驗證。
3

不要把『合法的 IP』與『真實的 IP』混為一談

淨化是衛生處理。形態正確的 IP 也可能是被偽造的值。真實 IP 的信任,始終靠②的 proxy 設定來保證。

驗證:修好後重現攻擊來確認

把『自以為修好了』變成『確實止住了』。①只在一個環境套用對策 → ②自己傳送同樣的非法標頭 → ③確認錯誤數不增加 → ④確認正常存取也照常運作 → ⑤沒問題再推到全部環境。分階段套用能防止事故。

續篇:堵了 IP,又被戳了 User-Agent(打地鼠的陷阱)

加上淨化數十分鐘後,又來了同一類錯誤。這次壞掉的是另一個欄——不是 ip_address 而是 user_agentIP 標頭被擋下的攻擊者,把同樣的惡意酬載換裝到了 User-Agent 標頭上。這裡需要轉換思路。

入口:XFF・User-Agent・Referer・… (多到數不清)
↓ 其中『會被保存』的是
出口:sessions 表裡的 ip_address 與 user_agent 這兩個而已
↓ 所以該淨化的入口是
只需通向那兩個的標頭(IP 系 + User-Agent)即可
一個個去堵『哪個標頭危險』就是打地鼠。從『不可信的值最終被保存到哪裡(出口)』反推,該堵的入口就確定了。

正確的問題不是『哪個標頭危險』,而是『不可信的值最終會被保存到哪個 DB 欄』。如果保存目標只有 ip_addressuser_agent 這兩個,那麼只要淨化通向這兩個的入口(IP 系標頭+User-Agent,保險起見連 Referer 也加上),源自工作階段的錯誤在原理上就能止住。不會被保存的其他標頭則無法橫向擴展。正常的 User-Agent 是 ASCII/正常的 UTF-8,所以加上只剔除無效位元組的處理也毫無影響。

本站的視角:從出口反推(不要一個個去堵入口)

一個個去堵標頭是打地鼠——攻擊者能用的入口多到數不清。從出口(保存欄・輸出位置)反推,把通向那裡的入口一次全部梳理出來才是正解。正如『堵了 IP,又被戳了 User-Agent』所學到的,做對症處理去堵某個標頭時,要把抵達同一保存目標的其他入口一併堵上。而且第二次同樣在重現並確認止住後才推開。

更進一步的續篇:入口沒能完全擋住,靠出口(保存前一刻)守住了

把 User-Agent 也堵上、心想『這下沒問題』的緊接著,ip_address 欄又來了同樣的錯誤。入口的淨化已經部署,用自己傳送與攻擊相同的非法標頭來測試也無法重現。可在生產上卻復發——這裡是這個故事的高潮。

原因在於,框架的『用戶端 IP 判定』內部很複雜。多種標頭的寫法(X-Forwarded-ForForwarded・自訂標頭)、與 proxy 信任設定的組合、解析順序與快取……在這些內部路徑的某處,汙染值從與我們淨化過的值不同的另一條路線作為『用戶端 IP』復活了。在入口無法全部捕獲

✗ 在入口守

淨化請求的標頭 → 用戶端 IP 經內部另一條路線復活 → 被繞過

✓ 在出口守

在 DB 保存的前一刻檢查 → 無論中途怎樣被汙染都是『必經的一點』 → 無法繞過

入口(對進來的值做檢查)可能被內部的解析路徑繞過。出口(寫入 DB 的前一刻)是『必經的一點』,所以不會被繞過。

於是把思路掉轉 180 度,決定在出口(寫入 DB 的前一步)守住。繼承工作階段保存類,只替換產生保存值的那部分(概念上):保存用 IP『若非合法 IP 則為 null』,保存用 User-Agent『剔除非法位元組並限制長度』。無論用戶端 IP 判定在內部怎樣翻車,寫入 DB 的前一刻都必經檢查,所以無從繞過。把兩條攻擊路線各連打十幾次,錯誤也沒有增加,正常使用者的工作階段也一切正常。

本站的視角:最後的堡壘是『出口(使用的地方)』

安全的定式是『不要相信輸入,在使用的地方(出口)務必驗證・跳脫』。入口的檢查在『盡早攔截』上有效,但對於框架會在內部解析・加工的那類資訊(用戶端 IP 判定等),它可能繞過入口而復活。在傳給 DB・HTML・命令等危險位置的前一刻=必經的一點做驗證・跳脫,才是最後的堡壘。入口與出口是兩回事,兩者都要做。

最大的收穫:被吵鬧地攔下來更安全

這一次,攻擊不僅失敗,還以大量錯誤通知的形式被可視化了。聽上去諷刺,但這其實不壞。比起被悄無聲息地通過,被吵鬧地攔下來更安全。不要把錯誤通知當成『雜訊』丟掉,而要把它當作『異常偵測的感測器』來用——不止步於『實際損害已防住』,而是去修復『為什麼會一路抵達內部』,這才是本站的立場。

接下來閱讀

FAQ

QX-Forwarded-For 可以信任嗎?
A

原樣是不能信任的。XFF 是用戶端可以隨意填值的標頭,攻擊者既能偽造成看似真實的 IP,也能填入注入探測的字串。可以信任的,只有『自己放在前端、由信任代理附加的 XFF』。

Q沒有使用反向代理或 CDN 時該怎麼辦?
A

完全不信任 XFF(保持框架的預設值)。前端並沒有代理卻設成『信任所有代理』,誰都能把偽造的 IP 餵給應用程式。沒有相應架構時,什麼都不信任才是正解。

Q對 IP 標頭做淨化就安全了嗎?
A

注入探測和錯誤洪流會被止住,但那只是衛生處理(止血的對症療法)。即便能整理成『合法的 IP』,也不保證它就是『真實的 IP』。真實 IP 的信任,始終要靠信任代理的設定來保證。

Q明明改好了 IP 標頭,怎麼又冒出同樣的錯誤?
A

因為同一個惡意酬載被換裝到了另一個入口(例如 User-Agent 標頭),最終又抵達了同一個保存欄。一個個去堵標頭會變成打地鼠。請從『不可信的值最終會抵達哪個 DB 欄/輸出位置』反推,把通向那裡的入口(IP 系+User-Agent 等)一併淨化。