Thirteen years of HTTP request smuggling in OpenBSD’s relayd
A single-header conformance defect in relay_http.c left OpenBSD’s HTTP reverse proxy vulnerable to classic CL.TE request smuggling from the 5.2 release in 2012 onwards. Found by targeted source review against the RFC 9112 framing rules; fixed in −current on 2026-06-03 in a single commit.
What the defect was
OpenBSD’s relayd is the project’s daemon for reverse-proxying and load-balancing TCP, HTTP and HTTPS traffic to backend services. When forwarding an HTTP message whose framing uses Transfer-Encoding: chunked, relayd parsed the body correctly as chunked — but it did not remove a co-present Content-Length header before passing the message to the backend.
That is contrary to the explicit rule in RFC 9112 §6.1, which the practice reproduces below for the reader:
“A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field.”
— RFC 9112 (HTTP/1.1), §6.1, June 2022
The conformant behaviour for any HTTP proxy in this situation is either to reject the request or to strip the Content-Length header before forwarding. relayd did neither: it parsed using the chunked framing it had chosen, and forwarded both headers to the backend.
Where the backend prefers Content-Length when both headers are present — which many HTTP backends do — the proxy and the backend disagree on where one request ends and the next begins on the same TCP connection. The disagreement is the smuggling primitive. The textbook label for this variant is CL.TE: front-end uses Content-Length (here, the back-end), back-end uses Transfer-Encoding (here, relayd — or vice versa). The effect is identical.
The practical operational impact, on any deployment where relayd sits in front of an HTTP backend with a divergent framing preference, is the standard CWE-444 set:
- Security-control bypass — a smuggled second request slips past authentication checks, WAF rules, or path-based ACLs that
relaydenforces on the visible first request. - Request-routing manipulation — the smuggled bytes can target backend endpoints or virtual hosts that the operator believed were guarded.
- Downstream consequences — cache poisoning, session-stealing on shared keep-alive connections, and other CWE-444 downstream effects, depending on the backend’s posture.
The practice has deliberately not published exploit payloads. The point of the disclosure is to allow operators to patch, not to ship a working smuggling crowbar against unpatched deployments. The bytes-on-the-wire required to exploit any given (relayd + backend) pairing depend on the backend’s exact framing preference and are not enumerated here.
Why this survived thirteen years
The current HTTP forwarding code in relay_http.c dates from OpenBSD 5.2 (released November 2012). The defect has been latent from that release through OpenBSD 7.9 inclusive — thirteen years of release branches.
HTTP request smuggling as a class was first systematised in 2005 (Linhart, Klein, Heled and Orrin, published by Watchfire) and then popularised across the security industry by PortSwigger’s 2019 research, after which most large-scale front-end proxies (Apache, nginx, HAProxy, IIS, CDN edges) were audited and updated against the specific RFC-conformance rules that govern conflicting framing.
relayd did not get that scrutiny. The reasons are reasonable in hindsight:
relayd’s deployment patterns have historically leaned toward TLS termination in front of OpenBSD-native services (httpd, OpenSMTPD, custom backends) rather than fronting third-party HTTP stacks with quirky header parsers. Where both the front-end and the back-end follow the same RFC discipline, the disagreement that powers smuggling does not arise.- The code path is internally used — a small daemon written and maintained inside the OpenBSD project, well away from the cross-vendor audit attention that the headline proxies got post-2019.
- The defect is small (a single missing header-strip), confined to one function in one file, and not the kind of thing a generic static analyser flags. It is the kind of thing a targeted source-review pass against the RFC text catches.
Which is what happened in this case.
relay_http.c with RFC 9112 §6.1 open in the other window and asking a single question of each header-processing branch: does this code do what the RFC requires when both Content-Length and Transfer-Encoding are present? That is exactly the kind of bounded, RFC-anchored source review a small lab can deploy without large infrastructure, and it is the kind of bug fuzzing and generic static analysis are statistically unlikely to find — the trigger is “send a well-formed HTTP message with both framing headers,” which a fuzzer can produce but which neither the proxy nor the backend treats as malformed in isolation.
The fix
The fix landed in OpenBSD −current on 2026-06-03 in commit e8e5aa2db9cf7bcd254dadc5f14a69547006e9a2. The change lives in usr.sbin/relayd/relay_http.c and is small — when relayd determines that an inbound HTTP message uses chunked framing, the Content-Length header is stripped from the outbound message before forwarding to the backend, per RFC 9112 §6.1.
Fix availability
The commit is in OpenBSD −current as of 2026-06-03. Release timing and any backport to the supported −stable branches are the OpenBSD project’s call; the practice does not speculate on them.
The authoritative sources are:
- openbsd.org — current released version and release notes
- openbsd.org/faq/faq5.html — release / −current / −stable policy
- openbsd.org/errata79.html — OpenBSD 7.9 errata
- openbsd.org/errata78.html — OpenBSD 7.8 errata
- github.com/openbsd/src — tree mirror
- cvsweb.openbsd.org/log/src/usr.sbin/relayd/relay_http.c,v?sort=File — canonical CVS view of the affected file
Interim posture for operators running relayd in HTTP forwarding mode (i.e., not pure TLS termination or layer-4 TCP relay) on OpenBSD 7.9 or earlier: either run a −current snapshot of relayd against the patched relay_http.c, or front relayd with a proxy known to reject conflicting Content-Length and Transfer-Encoding headers per RFC 9112 (current-vintage nginx or HAProxy, for example), so that no smuggled framing reaches relayd in the first place. Deployments using relayd purely for TLS termination or layer-4 TCP relay are unaffected; the HTTP code path is not entered.
Timeline
| Date | Event |
|---|---|
| 2012-11 | OpenBSD 5.2 released; current relay_http.c HTTP forwarding code present in this form. Defect latent from here onward. |
| 2026-05-28 | Reported to bugs@openbsd.org. |
| 2026-06-03 | Fixed in OpenBSD −current in commit e8e5aa2db9c. |
| 2026-06-04 | This case study published. |
What this case study is not
- Not an exploit drop. Specific bytes-on-the-wire payloads are deliberately not published. The find is enough for operators to patch; it is not enough for someone to use it as a smuggling crowbar against an unpatched deployment without independent work.
- Not a criticism of OpenBSD. Thirteen years of latency in a small internally-maintained daemon is not negligence; it is the survival pattern of a defect that needs a specific RFC-conformance test that nobody had run against this code. The find is the win; the speed of the maintainer response is the credit.
- Not a CVE request from the practice. The OpenBSD project does not generally request CVEs for defects in code maintained inside the project; the upstream commit is the record of the work. If a CNA chooses to assign retroactively, the practice will note it on this page.