HTTP Header Injection / CRLF Injection
HTTP headers are the metadata lines around a response: cookies, redirects, content type, caching, security headers. Header injection happens when an application lets untrusted input influence those header lines.
In real incidents: CRLF injection often hides in redirects and proxy headers. It feels harmless until it becomes cache poisoning or response splitting.
CRLF injection is the classic way this happens: if user-controlled data can contain âline breaksâ that the server or an intermediary treats as header separators, an attacker may be able to add or alter headers in the response.
Why header/CRLF injection happens (deep reason)
- HTTP is line-based: historically, headers are âlinesâ separated by CRLF. If attacker data can create new lines, it can create new headers.
- Apps build headers from input: redirects (
Location), filenames (Content-Disposition), debug headers, tenant routing, tracking IDs. - Trust boundaries are messy: your code, framework, reverse proxy/CDN, and browser may parse/normalize headers differently.
- Encoding/decoding mismatch: one layer decodes/normalizes input (percent-decoding, Unicode normalization) and another layer interprets it as header syntax.
Mental model: âframing vs dataâ
Think of an HTTP response as two parts:
- Framing: status line + header lines that control browser and intermediary behavior.
- Body: the content (HTML/JSON/image).
Header/CRLF injection occurs when untrusted input crosses into framing. Even if the body is safe, a malicious header can change meaning: redirect users, set cookies, weaken caching rules, or disable security policies.
Vulnerable vs secure code patterns (Node.js)
Vulnerable pattern (minimal)
// Node/Express (example)
app.get("/go", (req, res) => {
// â Untrusted input placed into a response header (redirect)
const next = String(req.query.next || "/");
res.setHeader("Location", next);
res.status(302).end();
}); Secure pattern (defensive)
// â
Allow-list + safe URL construction + safe defaults
function safeRedirectTarget(raw) {
const value = String(raw || "");
// 1) Allow only relative paths (no scheme/host) to prevent open redirects & header abuse
if (!value.startsWith("/")) return "/";
// 2) Block control characters defensively (even if runtime rejects later)
if (/[\r\n\u0000-\u001F\u007F]/.test(value)) return "/";
// 3) Optional: constrain to known prefixes
const allowedPrefixes = ["/account", "/orders", "/help"];
if (!allowedPrefixes.some(p => value === p || value.startsWith(p + "/"))) return "/";
return value;
}
app.get("/go", (req, res) => {
const target = safeRedirectTarget(req.query.next);
res.redirect(302, target);
}); Another common sink: Content-Disposition
// â Risky: user controls filename inside Content-Disposition
app.get("/download", (req, res) => {
const name = String(req.query.name || "file.txt");
res.setHeader("Content-Disposition", `attachment; filename="${name}"`);
res.type("text/plain").send("...");
});
// â
Safer: sanitize + fallback + avoid surprising characters
function safeFilename(raw) {
const s = String(raw || "").trim();
if (!s) return "file.txt";
// Remove control chars and quotes/semicolons that can break header parameters
const cleaned = s.replace(/[\r\n\u0000-\u001F\u007F"]/g, "_").replace(/[;=]/g, "_");
// Keep it reasonably short
return cleaned.slice(0, 80) || "file.txt";
}
app.get("/download", (req, res) => {
const name = safeFilename(req.query.name);
res.setHeader("Content-Disposition", `attachment; filename="${name}"`);
res.type("text/plain").send("...");
});
Where it still happens in modern stacks
- Redirect handlers: login âreturnTo/nextâ parameters and SSO callbacks.
- Downloads:
Content-Dispositionfilename/header parameters. - Custom security headers: building
Content-Security-PolicyorPermissions-Policydynamically from input/tenant config. - Proxy-added headers: app forwards user input to upstream services that set headers on its behalf.
- Legacy/heterogeneous components: mixed Node + older gateways, old load balancers, or custom middleware that manually writes raw HTTP.
- Header-like protocols: systems that parse âheader-ishâ text: SMTP/HTTP forwarding, log lines, CSV exports, or notification templates.
Variants (the âwhy they differâ)
1) Response header injection
Untrusted input changes a header value, or adds an additional header via boundary confusion. Impact depends on which header is influenced (cookies, redirects, caching, security policies).
2) Response splitting (concept)
If a component interprets attacker-controlled data as âend of headersâ, it can accidentally treat the remainder as a new response segment. In practice, modern stacks often block this, but itâs still a useful mental model: your input must never control where âheaders endâ.
3) Request header injection (app-to-upstream)
The server becomes the client: it forwards a header to an upstream service using untrusted input. This can cause SSRF-like effects, cache poisoning, auth bypass in upstream routing, or log/metric confusion.
4) Proxy normalization mismatches
One layer normalizes/decodes, another layer parses. The same bytes can be interpreted differently across layers, creating bypasses even if your appâs runtime blocks obvious newline characters.
Detection workflow (experienced-style, systematic)
This is a defensive verification workflow. The goal is to find and prove unsafe header construction without giving exploit recipes.
Step A â Inventory header-writing features
- Redirect endpoints (
Location) and login return URLs - Download endpoints (filenames) and content type negotiation
- Any endpoint reflecting input into headers (debug headers, correlation IDs, tenant routing)
- API gateways/proxies that forward request headers upstream
Step B â Establish baselines
- Capture a normal response and note: status code, headers present, and any dynamic header values.
- Repeat to confirm stability (avoid claiming issues from one noisy response).
Step C â Look for âheader sensitivityâ
- Does changing a single input cause unexpected headers to appear/disappear?
- Do security headers (CSP/HSTS/CTO) change based on user input or tenant config?
- Do intermediaries (CDN/proxy) behave differently than direct origin?
Step D â Validate defenses in the stack
- Does the app/framework reject invalid header characters (errors, dropped headers)?
- Is there a reverse proxy doing its own rewriting/normalization?
- Are there multiple hops (app â gateway â service) where parsing could differ?
How to prove safely (evidence checklist)
A professional proof for header injection focuses on observable header behavior and impact, without forcing destructive side effects.
Evidence checklist
- Baseline capture: response headers from a normal request (include date/time and environment).
- Controlled variation: change only one input and capture the full response headers again.
- Diff: highlight exactly which header(s) changed and why that matters (redirect, cookie flags, caching, security policy).
- Stack trace / error capture: if the runtime rejects invalid header values, capture the error as evidence of a protected sink (still useful for code review).
- Impact statement: âThis allows altering header X which often leads to impact Y under these constraints.â
Exploitation progression (attacker mindset)
This explains attacker decision-making at a high level (no copy/paste instructions). Attackers usually start by proving any influence on a privileged header channel, then search for the highest-impact header.
Phase 1: Find header sinks
- Redirect endpoints, download filenames, debug/correlation headers, tenant routing headers.
- Places where user input is likely used âfor convenienceâ.
Phase 2: Prove header influence
- Confirm that a request parameter predictably affects a header value.
- Look for inconsistencies between origin vs proxy/CDN responses.
Phase 3: Target high-impact headers
- Redirect behavior: can users be sent somewhere unexpected?
- Cookie semantics: can cookie flags or extra cookies appear?
- Caching behavior: can cache keys or cache-control be influenced?
- Security policy: can CSP/CTO/HSTS headers be altered or removed?
Phase 4: Chain with other bugs
- Combine with open redirect, XSS, cache poisoning, or auth flows to increase impact.
- Use application-specific behavior (SSO, CDN caching) rather than relying on âone generic trickâ.
Tricky edge cases & bypass patterns (what attackers look for)
- Multi-layer decoding: one layer decodes/normalizes input (URL decode, Unicode) and another layer interprets it differently.
- Different parsers per hop: Node app, reverse proxy, CDN, and browser can have different strictness.
- Header parameter parsing: headers like
Content-Dispositionhave their own grammar; escaping mistakes can change meaning. - Duplicate headers: multiple instances of a header can behave differently across clients (first-wins vs last-wins).
- Cache interactions: header changes can affect caching. Even without âsplittingâ, unsafe header control can enable cache poisoning patterns.
- Security header drift: tenant-configured CSP/policies that allow unsafe directives or remove protections.
- Logging/metrics injection: headers reflected into logs can create misleading entries (not always a security boundary, but can hide attacks and complicate incident response).
Safe validation workflow (defensive verification)
- Identify sinks: redirects, downloads, debug headers, tenant routing, proxy forwarding.
- Baseline: capture full response headers and confirm stability.
- Single-variable test: change one input, capture headers again, and diff.
- Check intermediaries: compare origin vs through CDN/proxy; note differences.
- Assess impact: map the affected header to real behaviors (redirect/cookies/cache/security policy).
- Regression idea: add tests ensuring header values reject control characters and enforce allow-lists for targets/filenames.
Defensive patterns & fixes (Node.js)
Pattern 1: Allow-list for redirect targets
// â
Allow-list routes instead of trusting user-provided URLs
const allowed = new Set(["/account", "/orders", "/help"]);
app.get("/go", (req, res) => {
const raw = String(req.query.next || "");
const next = allowed.has(raw) ? raw : "/account";
res.redirect(302, next);
}); Pattern 2: Central header setter with validation
// â
One place to enforce header safety rules
function setSafeHeader(res, name, value) {
const n = String(name);
const v = String(value);
// Block control chars (CR/LF and other non-printables) in both name and value
if (/[\r\n\u0000-\u001F\u007F]/.test(n) || /[\r\n\u0000-\u001F\u007F]/.test(v)) {
throw new Error("Invalid header characters");
}
// Optional: restrict which headers can be set dynamically
const dynamicAllow = new Set(["X-Request-Id", "X-Tenant", "Content-Disposition"]);
if (!dynamicAllow.has(n)) {
throw new Error("Header not allowed to be dynamic");
}
res.setHeader(n, v);
}
app.get("/download", (req, res) => {
const file = safeFilename(req.query.name);
setSafeHeader(res, "Content-Disposition", `attachment; filename="${file}"`);
res.type("text/plain").send("...");
}); Pattern 3: Prefer framework helpers
- Use
res.redirect()instead of manually constructing status +Location. - Use safe libraries/helpers for
Content-Dispositionwhen possible, but still apply policy (allow-list and constraints). - Do not manually write raw HTTP response strings.
Confidence levels (how sure are you?)
- Low: a header changes unexpectedly once, but you canât reproduce or it correlates with caching/proxy noise.
- Medium: reproducible header influence in a low-impact header (e.g., debug/correlation) or blocked attempts evidenced by runtime errors.
- High: reproducible control over a meaningful header (redirect/caching/security policy/cookie semantics) with clear evidence and constraints understood.
Checklist (quick review)
- Do any endpoints set headers directly from user input (
Location,Content-Disposition, debug headers)? - Are redirect targets allow-listed (prefer relative paths and known routes)?
- Are header values validated to reject control characters and unexpected delimiters?
- Are security headers (CSP/HSTS/CTO) static and consistent (not user/tenant-controlled without strict policy)?
- Are there intermediaries (CDN/proxy) that rewrite or normalize headers? Is behavior consistent?
- Are there tests to prevent regressions for header setters and redirect/download features?
- Are logs/metrics safe from header-driven injection that could hide or confuse incident response?
Remediation playbook
- Contain: disable the risky behavior (dynamic redirect/header) or temporarily force safe defaults.
- Fix root cause: apply allow-lists for redirect targets and sanitize constrained header parameters (filenames), rejecting control characters.
- Centralize: implement a single safe header-setting helper; remove ad-hoc
setHeadercalls that accept raw user input. - Scope: search for similar patterns across codebase (redirect handlers, downloads, debug headers, proxy forwarding).
- Test: add unit + integration tests for âcontrol characters rejectedâ and âonly allow-listed targets allowedâ.
- Harden the edge: configure proxies/CDNs to reject invalid header chars and normalize safely; keep security headers consistent.
- Monitor: alert on unusual redirect targets, security header drift, and repeated header validation errors.
Interview-ready answers (60-second + 2-minute)
60-second answer
Header injection happens when untrusted input influences response headers like Location or Content-Disposition. CRLF injection is the classic boundary problem: if attacker-controlled data can create new header lines, they can add/alter headers. I prevent it by treating headers as a privileged channel: allow-list redirect targets, validate/sanitize header parameters, centralize header setters, and add tests to ensure control characters and unexpected separators are rejected across the stack.
2-minute answer
I model this as a framing vs data failure: HTTP headers are protocol metadata that controls browser and intermediary behavior, so untrusted input should never be able to affect header boundaries or sensitive header semantics. In practice I inventory sinks (redirects, downloads, debug headers, and proxy forwarding), establish baselines, then check for repeatable header diffs. I also consider multi-layer parsing differences between app, proxy/CDN, and browser. Fixes are policy-driven: allow-list redirect targets (prefer relative paths), sanitize constrained header parameters like filenames, and centralize header setting through a safe helper that blocks control characters. Finally I prevent regressions with automated tests, consistent edge configuration, and monitoring for security header drift and unusual redirects.
Interview Questions & Answers (Easy â Hard)
Easy
- What is HTTP header injection?
A: Layman: Itâs when user input changes the âlabelsâ around a response, like redirect or cookie settings. Deep: Headers are a privileged metadata channel; if untrusted data flows into header construction, it can change browser/proxy behavior. Prevent by allow-lists and centralized safe header setters. - What is CRLF injection In plain terms?
A: Layman: Itâs when line breaks sneak into a header value and the system treats it like a new header line. Deep: CRLF historically separates header lines. If attacker input can create a boundary, it can add/alter headers. Modern stacks often reject it, but you still defend via policy + validation. - Why are response headers âhigh impactâ?
A: Layman: They tell the browser what to doâredirect, store cookies, cache, or apply security rules. Deep: ChangingLocation, caching headers, or security headers (CSP/HSTS) can meaningfully change application security posture without touching the response body. - Name two common places this shows up.
A: Layman: Redirect links and file downloads. Deep: Redirect parameters (next/returnTo) affectLocation; downloads affectContent-Dispositionfilename parameters. Both often use user input âfor convenienceâ. - Whatâs the best primary fix?
A: Layman: Donât put raw user input into headers. Deep: Apply strict allow-lists for redirect targets and constrained sanitization for header parameters, plus reject control characters and centralize all dynamic header setting. - Do modern frameworks already prevent this?
A: Layman: They help, but you shouldnât rely only on it. Deep: Some runtimes reject invalid header characters, but bypasses can occur through legacy components, proxies, custom raw header building, or downstream services that parse differently.
Medium
- Scenario: login has
?next=.... What risks do you think about?
A: Layman: Users might be redirected somewhere unsafe. Deep: I consider open redirect and header abuse viaLocation. I fix by allow-listing internal routes (relative paths), rejecting control characters, and using framework redirect helpers. - Scenario: download endpoint uses user-provided filename. What can go wrong?
A: Layman: The download header might be malformed or dangerous. Deep:Content-Dispositionhas its own grammar; unescaped characters can alter meaning. I constrain allowed characters, remove control chars, keep filenames short, and use safe helpers when possible. - How do you validate this issue safely in a test?
A: Layman: Compare response headers before and after changing one input. Deep: I baseline the headers, vary one parameter, capture full headers again, and diff. I avoid destructive side effects, and I check behavior through proxies/CDNs because parsing can differ. - Why do proxies/CDNs matter for header injection?
A: Layman: They can change how responses are handled. Deep: Different hops have different parsers and normalization. A value rejected by one layer might be interpreted differently by another. I validate origin vs edge behavior and keep security headers consistent. - Follow-up: Why is âsanitize special charactersâ not enough?
A: Layman: You can miss a case. Deep: Blacklists are brittle and encoding can reintroduce dangerous characters. A better strategy is a strict policy: allow-list acceptable redirect targets and constrain header parameters to a safe character set plus length limits. - Follow-up: What should be centralized?
A: Layman: The logic that sets headers. Deep: I centralize dynamic header setting through one helper that enforces âno control charsâ, restricts which headers are allowed to be dynamic, and is covered by unit tests. - Scenario: app forwards a user-controlled header to an upstream service. Risk?
A: Layman: It could confuse or trick the upstream system. Deep: Upstreams may use headers for routing/auth/logging. Untrusted forwarded headers can cause authorization or caching issues. Fix by allow-listing forwarded headers and setting server-derived values. - Scenario: security headers change per tenant based on config. Whatâs the pitfall?
A: Layman: Some tenants might become less protected. Deep: Tenant-configured CSP/permissions can accidentally allow unsafe directives or remove protections. Fix by policy constraints: validated templates, safe defaults, and explicit disallow of dangerous relaxations.
Hard
- Scenario: You see different header behavior at origin vs CDN. How do you reason about it?
A: Layman: Something in the middle is rewriting or parsing differently. Deep: I suspect normalization/decoding differences or header rewriting rules. I compare raw header sets, cache keys, and transformations at each hop, and I harden edge config to reject invalid header chars consistently. - Scenario: A cache serves a response with unexpected headers to other users. How could header control relate?
A: Layman: Bad headers could get âsavedâ and reused. Deep: If untrusted input influences caching headers or cache keys, you can create cache poisoning-like behavior. Fix by ensuring cache keys donât include untrusted header variations and by making caching rules deterministic and safe. - Whatâs the difference between âvalue controlâ and âframing controlâ in HTTP headers?
A: Layman: Changing a normal value is different from breaking the structure. Deep: Value control means a header value changes within expected grammar; framing control means input can change header boundaries or create new headers. Framing control is higher impact and must be categorically blocked. - How do duplicate headers complicate security?
A: Layman: Different systems might pick different ones. Deep: Some clients take first-wins, others last-wins; proxies may merge or drop duplicates. This can create bypasses for policy headers. Fix by emitting single authoritative headers and validating proxiesâ behavior. - Follow-up: If Node rejects invalid header characters, is the issue âclosedâ?
A: Layman: Not always. Deep: You still need policy for redirect targets and header parameters, because even âvalidâ values can be dangerous (open redirect, unsafe CSP). Also, other components (proxies/upstreams) might still be vulnerable if they parse differently. - Follow-up: What tests do you add after a fix?
A: Layman: Tests that confirm headers canât be manipulated. Deep: Unit tests for the safe header helper (reject control chars, enforce allow-lists), integration tests for redirect/download endpoints, and checks that security headers remain stable across tenants and environments. - Follow-up: How do you find similar issues in a large codebase?
A: Layman: Search for where headers are set. Deep: I grep forsetHeader,writeHead,redirectusages, and places where request parameters flow into response headers. Then I replace ad-hoc patterns with centralized helpers and add lint rules. - Scenario: An endpoint sets CSP dynamically based on a query param for âpreview modeâ. How do you handle it?
A: Layman: Thatâs risky because it weakens security based on user input. Deep: CSP should be policy-controlled, not user-controlled. I remove query-based CSP changes, use safe server-side feature flags, and enforce that CSP relaxations require explicit, audited configuration with strict constraints.