資安指南

X-Forwarded-For(XFF)偽造是什麼:信任代理的設定錯誤與防禦

X-Forwarded-For(XFF)是用戶端可隨意偽造的 HTTP 標頭。以個人開發常見事例說明夾帶注入探測的掃描、「信任所有代理」設定的陷阱,以及如何修正信任代理設定(不提供攻擊步驟)。

發布於 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 裡?』

如果應用程式做得馬虎,SQL 注入 或 XSS 就會經由 XFF 成功。這是把『承載 IP 的標頭』濫用為 傳送任意字串的地方 的手法。

為什麼沒有損害:兩道防護

防護① 值的繫結(佔位符)

沒有把 IP 值做字串拼接,而是用佔位符把它作為資料交給 DB。輸入不會被解讀為指令,所以 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_agent。IP 標頭被擋下的攻擊者,把同樣的惡意酬載換到了 User-Agent 標頭上。這需要換一種做法。

入口:XFF・User-Agent・Referer・… (多到數不清)
↓ 其中『會被保存』的是
出口:sessions 表裡的 ip_address 與 user_agent 這兩個而已
↓ 所以該淨化的入口是
只需通向那兩個的標頭(IP 系 + User-Agent)即可
一個個封鎖危險的標頭,攻擊只會轉到另一個標頭。從不可信的值最終被保存到哪裡(出口)反推,需要處理的入口就成了一份固定且簡短的清單。

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

本站的視角:從出口反推,而不是一個個封鎖入口

一個個去堵標頭行不通;攻擊者總能改用另一個標頭。從出口(保存欄・輸出位置)反推,把通向那裡的入口一次全部梳理出來。正如這次攻擊從 IP 標頭轉到 User-Agent 所示:修補某個標頭時,要把抵達同一保存目標的其他入口一併處理。而且第二次同樣在重現並確認止住後才推開。

更進一步:在值被保存的前一刻防禦

把 User-Agent 也處理好之後,ip_address 欄又來了同樣的錯誤。入口的淨化已經部署,用自己傳送與攻擊相同的非法標頭來測試也無法重現。可在生產上錯誤卻一再出現——這是這個案例的重點。

原因在於,框架的『用戶端 IP 判定』內部很複雜。多種標頭的寫法(X-Forwarded-For・Forwarded・自訂標頭)、與 proxy 信任設定的組合、解析順序與快取……在這些內部路徑的某處,惡意的值從與我們淨化過的值不同的另一條路線,作為『用戶端 IP』再次出現。只在入口檢查,無法全部攔下。

✗ 在入口守

淨化請求的標頭 → 惡意的用戶端 IP 經內部另一條路線再次出現 → 被繞過

✓ 在出口守

在 DB 保存的前一刻檢查 → 每個值都必經的地方 → 無法繞過

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

於是改變做法,在出口(寫入 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 等)一併淨化。