揭開 Nginx CRLF 注入的真相!
摘要
報告將 CRLF 驅動的資料流不同步視為邊界完整性失效,而非孤立的標頭注入(header-injection)缺陷。主要研究顯示,百分比解碼後的換行字元可以改變 Reverse Proxy 轉發的 Request ,使得 Proxy 與上游解析器對 Request 數量、訊息長度或回應所有權產生不同認知 [1] 。這條鏈可能導致 Request 分割、回應佇列污染、CL.TE 不同步、快取操作、存取控制繞過、瀏覽器媒介執行,以及 Session 資料暴露。可利用性取決於正規化、Connection reuse、框架、回應大小和瀏覽器行為。因此防禦必須移除不安全的插入點,並保留明確的上游框架。
1. 研究問題與系統模型
低影響的標頭注入操作(primitive)如何變成多使用者資料流不同步狀況?答案在於架構。Reverse Proxy 終止一個串流,並為上游服務建構另一個 Request。如果它對 Path 或變數的正規化方式與上游解析器不同,編碼後的 CRLF 就會變成真正的分隔符。前端可能認為它轉發了一個 Request ,而上游卻解析出兩個 Request 或不同的 Body 邊界:這是一種透過持久串流展現的解析器差異。
主要文章中的 Nginx 範例明確展示了這種轉換。
清單 1. 不安全的 Nginx 轉發設定
- # nginx.conf
- http {
- upstream backend {
- server backend.internal.com:8000;
- }
- server {
- location / {
- proxy_pass http://backend$uri;
- }
- }
- }
$uri
直接放入
proxy_pass
。在主要研究中,Nginx 在正規化期間會對編碼的 CRLF 進行 URL 解碼,因此攻擊者控制的 Path 位元組可以成為上游的換行符。安全邊界不是用戶端可見的 Request ,而是發送給上游解析器的後正規化串流。此設定重現自主要文章,並非建議的部署模式
[1]
。
圖 1. CRLF 到回應佇列路徑。
2. 從標頭注入到 Request 分割
第一個實驗步驟有可觀測性。主要研究建議注入無效語法,該語法應產生可預測的狀態碼:無效的協定版本可產生 505,而無效的 Transfer-Encoding 值則可產生 501。這些回應是差異預言(Differential oracle),顯示注入的位元組存活下來通過前端轉換,並在上游被解釋。
以下配對的 Request 和回應說明為何必須分別分析表面上的 Path 和轉發的 Request 。
清單 2. 編碼的 CRLF 改變了上游 Request 結構
- GET /%20HTTP/1.1%0d%0aContent-Length:%20X%0d%0aX:%20x HTTP/1.1
- Host: example.com
- GET / HTTP/1.1
- Content-Length: X
- X: x HTTP/1.1
- Host: example.com
- HTTP/1.1 400 Bad Request
Content-Length
欄位,以及在原始 Host 標頭之前的額外類標頭片段。400 回應之所以有用,是因為它是上游解析的確定性證據,而非因為它本身就是影響。一旦分隔符可以建立有效的第二個 Request ,相同的操作(primitive)就可以變成 Request 分割和回應佇列污染
[1]
。
當第二個 Request 完整時, Request 分割會變得嚴重。上游會產生兩個回應,但預期只有一個回應的前端會將額外的回應保留在佇列中。在重複使用的 TCP 連線上,下一個用戶端 Request 可能會收到佇列中的回應。Academy 證實,Connection reuse、完整的獨立 Request 以及連線持久性是關鍵的先決條件 [4] 。因此,機密性和可用性是相互關聯的:在使用者收到錯誤回應的同時,不相關的回應也可能被收集。
3. 框架差異與 CL.TE 資料流不同步
CRLF 注入也可以添加框架標頭,而不是第二個 Request 邊界。在主要研究中,注入的
Transfer-Encoding: chunked
標頭與用戶端提供的
Content-Length
結合。前端可能使用長度值,而上游則採用 chunked framing,或者相反。Timeout 探測很有價值,因為它可以測試前端是否仍在等待上游已認為完整的位元組。
清單 3. 主要研究中的 CL.TE timeout 探測
- POST /<@urlencode_all> HTTP/1.1
- Transfer-Encoding: chunked
- Foo: bar</@urlencode_all> HTTP/1.1
- Host: example.com
- Content-Length: 13
- d
- x=y
- 0
- -TIMEOUT-
僅掃描狀態碼是不夠的。目標可能會拒絕兩個連續 Request 並關閉連線,但卻接受一個注入的標頭並進入 CL.TE 狀態。相反地,堆疊的回應可能是由於過度讀取或普通的流程化(pipelining)所導致。可靠的分析需比較解析器行為、連線生命週期、邊界、回應順序以及受控的 Payload 變化。
4. 瀏覽器傳遞的資料流不同步
主要研究將威脅模型從客製化用戶端擴展到瀏覽器。許多 CRLF 驅動的案例可以透過 navigation 或
fetch
發出,將執行環境移到受害者的連線環境中。補充研究指出,跨來源(cross-origin)的 header 和 Body 受到限制,而 HTTP/1.1 connection reuse 對用戶端案例仍然很重要
[3]
。主要研究表明,當直接的 cross-user reuse 不可行時,
CRLF 注入
仍然可以提供此種瀏覽器相容路徑。
清單 4. 主要研究中與瀏覽器相容的 Request 建構
- fetch(
- "https://example.com/%20HTTP/1.1%0d%0a
- Transfer-Encoding:%20chunked%0d%0a
- Foo:%20bar",
- {
- method: "POST",
- body: "0\r\n\r\nTRACE / HTTP/1.1\r\nX: x"
- }
- )
連線鎖定和 IP 鎖定的案例會改變影響路徑,而不是移除缺陷。HEAD 回應大小操作、利用 Range 的長度控制、iframe 重複和視窗排序使時序敏感的執行更加可靠。這些是疊加在原始解析器差異上的傳遞技術,因此評估應區分網路可達性和瀏覽器可達性。
5. 防禦意涵
主要研究建議避免在 Nginx
proxy_pass
或
return
指令中使用
$uri
和
$document_uri
,同時確保正規表達式衍生的變數排除空格和控制字元。審查應包含 OpenResty 和 Tengine,因為其衍生版本保留了正規化風險
[1]
。應在正規化之前進行驗證,並拒絕編碼的 CR、LF 和其他與框架相關的重要位元組,而不是在解碼後進行修復。
防禦也必須是系統性的。每個 hop 應使用 one request-length 演算法,移除衝突的框架標頭,並在解析失敗時關閉連線。除非能嚴格證明請求與回應的數量對應關係,否則回應佇列不應跨越不同的安全環境。比較 HTTP/1.1 的研究認為,HTTP/2 在上游消除了文字框架歧義的一大類問題,但轉換與實作上的缺陷仍需進一步驗證 [2] 。目標是 Request 位元組、解析訊息和回應之間的一對一對應。
6. 結論
CRLF 驅動的資料流不同步是一種組合性失效:正規化創造了語法,解析器差異創造了額外訊息,Connection reuse 傳播了錯誤,而瀏覽器操作(primitive)將執行環境重新定位到使用者 Session 中。主要文章將標頭注入重新定義為一個影響機密性、完整性和可用性的串流完整性問題 [1] 。嚴謹的測試應對每個轉換進行建模,確認回應佇列行為,並將時序、狀態碼和連線關閉視為解析器狀態的證據。安全的設定和明確的框架比針對 Payload 的過濾器更為持久。