Awareness

HTTP Header Injection: When Metadata Becomes Executable

Published  ·  15 min read

HTTP Header

Most security reviews look at the body of a request, the parameters, the JSON, the form fields, the file uploads, because that is where the interesting logic lives, and that is where developers expect user input to arrive.

The headers get treated as plumbing, as routing information, as boring metadata that the framework handles automatically, and that assumption is the vulnerability, because headers are user controlled, headers are parsed by every component in the chain, and headers decide what the application does next.

When a single carriage return lands in the wrong place, the metadata layer stops being metadata and starts being code.

Important Disclaimer

This article is intended for educational and defensive purposes only, and the techniques described here are shared to help security professionals test their own applications and harden their own infrastructure.

Do not use these techniques against systems you do not own or do not have explicit written permission to test, because unauthorized testing is illegal in most jurisdictions, and testing header handling against third parties can disrupt shared caches and affect other users.

The author assumes no liability for any damages, legal consequences, or other outcomes resulting from the use or misuse of this information, so always obtain proper authorization before conducting any testing, and stay legal, stay ethical, stay responsible.

Why Headers Are an Execution Layer

A request header is not a passive label, it is an instruction, and every component that touches the request makes decisions based on what the header says.

A load balancer reads the Host header and decides which backend to route to, a reverse proxy reads X-Forwarded-For and decides what IP to log, a cache reads the request and decides whether to store the response, a framework reads Accept-Language and decides what content to serve, and the application reads all of it and decides what to trust.

Each of those decisions is a place where the attacker's input becomes the system's behavior, and the reason header injection is so productive is that nobody audits the plumbing, because the plumbing is supposed to be boring.

Part One: CRLF Injection and Response Splitting

The carriage return and line feed characters are the separators that define the structure of HTTP, so a CRLF pair is what tells a parser that a header has ended and a new one has begun.

If an attacker can inject CRLF into a value that ends up in a response header, they can terminate that header early, and anything they write after it becomes a new header, and with two CRLF pairs they can end the header block entirely and write a response body of their own choosing.

Where It Lands in Practice

The classic sink is any header that reflects user input, and the most common examples are redirects, cookie values, custom tracking headers, and filenames that get placed into a Content-Disposition header.

A redirect is the richest target, because the Location header is where the user is sent next, and if the attacker controls the value after the CRLF, they control what the browser does when it reads the response.

What the Attack Achieves

  • The first outcome is header injection, which lets the attacker set a cookie, change the content type, disable a security header, or add a cache control directive that makes the response persist when it should not.
  • The second outcome is response splitting, which lets the attacker inject an entirely new response into the same connection, and that is where the damage escalates, because a poisoned cache can serve the attacker's response to every subsequent visitor.
  • The third outcome is cross site scripting, delivered not by injecting into the page body, but by injecting a response body and a content type that the browser will render as HTML.

Encoding Variants You Need to Test

A naive filter that blocks the literal characters is not a defense, because the attacker can encode them, and the parser may decode them at a different layer.

Worth testing are the plain characters, the percent encoded form, the double encoded form, the UTF-8 overlong encoding of the same characters, and the Unicode line separator variants, because each one may be treated differently by the front end, the proxy, and the framework.

How to Test It Practically

  • Start by finding every place where user input appears in a response header, and the easiest way to find them is to send a unique marker in each input and search the raw response for that marker.
  • Once you have a candidate, send the marker followed by an encoded CRLF and a second marker, then inspect the raw response, and if the second marker appears as its own header, you have injection.
  • Escalate carefully by adding a second CRLF and a small body, then confirm in a browser, and always do this against a system you own, because a successful split against a shared cache can affect other people.

The Defenses That Actually Work

  • Strip or reject CR and LF at the point of input validation rather than at the point of output, and use framework functions that handle header encoding for you instead of building header strings by hand.
  • Set the response through structured APIs, not through string concatenation, and configure the platform to reject malformed header values outright, because modern runtimes and servers will refuse to send a header containing a raw newline if you let them.

Part Two: Host Header Attacks

The Host header is the most trusted user controlled value on the internet.

It tells the server which site the client wanted, and in a shared hosting environment it is the only thing that distinguishes one tenant from another, and because it is essential to routing, most stacks treat it as authoritative rather than adversarial.

Why It Is Trusted

A generation of developers learned that the Host header is set by the browser and that users cannot change it, and that was never quite true, because anyone with a proxy can set it to whatever they like, and curl makes it a one flag operation.

The result is that a great many applications build absolute URLs from the Host header without ever asking whether the value is legitimate, and that single habit creates a family of attacks.

The Main Variants

  • Password reset poisoning is the most damaging, because the application generates a reset link using the Host header, and the attacker sends the reset request with a Host value pointing at a domain they control, so the victim receives a legitimate email containing a link to the attacker's server, and the token arrives with it.
  • Cache poisoning is the second, because a cache key often does not include the Host header, and an attacker can send a request with a malicious Host, cause the response to be cached, and have every subsequent visitor receive the poisoned response.
  • Routing based access control bypass is the third, because some architectures apply authentication at the front end based on the path, and then route to a backend based on the Host, so a Host value that reaches a different backend can skip the control entirely.
  • Virtual host confusion is the fourth, and it shows up when a default virtual host serves a different application, so a request with an unexpected Host lands somewhere it was never meant to reach.

The X-Forwarded Family

Behind a proxy, the original Host is often preserved in X-Forwarded-Host, and the original scheme in X-Forwarded-Proto, and the original client in X-Forwarded-For, and these headers are trivially spoofable unless the proxy overwrites them.

If the application trusts X-Forwarded-Host instead of Host, the attack surface does not shrink, it just moves, and the attacker sets whichever header the application happens to believe.

How to Test It Practically

  • Send the same request with a Host value you control, then look for that value in the response, in any email the application sends, in any generated link, and in any cached artifact.
  • Try the absolute URL form in the request line, try X-Forwarded-Host, try X-Forwarded-Proto, and try duplicate Host headers, because different layers pick different winners and the disagreement is where the vulnerability lives.
  • Do not test cache poisoning on a shared production cache, because a successful poison affects real users, and the ethical line is clear here.

The Defenses That Actually Work

  • Maintain an allowlist of valid hostnames and reject anything that is not on it, and do this at the edge so that no downstream component ever sees an unapproved value.
  • Do not build URLs from request headers, and if you must, use a configured canonical base URL instead, and never trust X-Forwarded headers unless your own proxy sets them and strips any client supplied version.

Part Three: Proxy Abuse and Header Trust

Every hop in the request path is a parser, and every parser has its own opinion about what the headers mean, and the gaps between those opinions are where attackers live.

The Trust Boundary Problem

A reverse proxy usually adds headers to describe what it saw, and the application usually trusts those headers, and the problem is that most proxies do not strip the client's version of the same headers before adding their own.

So the attacker sends an X-Forwarded-For with their chosen value, the proxy appends the real one, and the application reads the first value it finds, which is the attacker's, and every access control decision based on client IP is now attacker controlled.

The Cache Key Problem

A cache decides whether to store a response based on a cache key, and the cache key is usually derived from the URL and a subset of headers, and any header that influences the response but is not part of the key is a poisoning primitive.

That is the mechanism behind a whole class of bugs, where a header like X-Forwarded-Host changes what the page contains, and the cache stores that variant under the same key as the legitimate one.

The Routing Disagreement Problem

Front end and back end often disagree about which header takes precedence, and that disagreement is the basis of request smuggling, where a request that the front end considers finished is still being read by the back end.

Header injection feeds directly into that family, because a smuggled request can carry its own headers, and a back end that trusts them will act on them.

How to Test It Practically

  • Map the path first, and identify every component between the client and the application, because you cannot reason about trust boundaries you have not located.
  • Then test each hop for the headers it adds, the headers it strips, and the headers it passes through unmodified, and pay particular attention to anything the application treats as authoritative.

The Defenses That Actually Work

  • Strip client supplied versions of every forwarded header at the edge, and set them yourself with values you control, and treat the edge as the only place where identity and origin are established.
  • Keep the cache key aligned with the response, so that anything which changes the output is also part of the key, and prefer to disable caching for responses that depend on request context you cannot fully control.
  • Keep front end and back end parsing consistent, and update both together, because a mismatch in how they read framing is the foundation of smuggling.

Real Scenarios

Scenario 1: The Reset Link That Went Somewhere Else

The Setup

A web application offers password reset by email, and the email template builds the reset link using the Host header of the request.

The Attack

An attacker submits a reset request for a victim's account, and sets the Host header to a domain they control, and the application sends a legitimate email to the victim containing a link to the attacker's domain with the real reset token attached.

The Result

The victim clicks the link, the token is delivered to the attacker's server, and the attacker uses it to reset the password and take the account.

The Lesson

The email was genuine, the token was genuine, and the domain was not, and nothing in the chain flagged it.

Scenario 2: The Cached Redirect

The Setup

A front end proxy caches responses based on the URL, and a backend endpoint redirects users based on a parameter.

The Attack

An attacker sends a request that includes a CRLF injected into the parameter, resulting in a response from the server that is accompanied by a secondary response that redirects the client to a malicious site, and the proxy caches the combined result.

The Result

Every subsequent visitor to that URL receives the poisoned response and is redirected to the attacker's page.

The Lesson

The injection happened in one request, and the cache delivered it to thousands.

Scenario 3: The Spoofed Client IP

The Setup

An internal application allows access based on the client IP, and it reads X-Forwarded-For because it sits behind a proxy.

The Attack

The attacker sends a request with an X-Forwarded-For value of a trusted internal address, and the proxy appends the real address without stripping the supplied one, and the application reads the first value it finds.

The Result

The attacker reaches an internal only endpoint from the internet.

The Lesson

The control existed, the control was bypassed, and the bypass was one header.

Defensive Measures That Actually Work

1. Validate Headers at the Edge

Reject malformed values, reject raw control characters, and reject anything that does not match an expected pattern, and do it before the request reaches the application.

2. Use an Allowlist for Host

Maintain the list of hostnames your application legitimately serves, and refuse everything else, and make the default virtual host a rejection rather than a fallback.

3. Never Build URLs From Requests

Use a configured base URL for links in emails, for redirects you generate, and for anything a user will click, and treat the request headers as untrusted input rather than as configuration.

4. Strip and Set Forwarded Headers

Remove any client supplied version of X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto at the edge, then set them yourself with values you control.

5. Align Cache Keys With Response Content

Include every header that influences the response in the cache key, or disable caching for those responses, and review this whenever you add a feature that varies output.

6. Keep Parsers Consistent

Keep front end and back end versions aligned, prefer HTTP/2 end to end where you can, and treat framing mismatches as a security issue rather than an operational quirk.

7. Use Structured Header APIs

Set response headers through framework functions rather than string concatenation, and let the platform reject invalid values, because most modern runtimes will refuse to emit a header containing a newline.

8. Log the Raw Request

Capture the raw headers as they arrived, not only the parsed version, because the interesting attacks live in the gap between what arrived and what the parser understood.

9. Test the Metadata Layer

Include header injection, Host manipulation, and forwarded header spoofing in your regular testing, because most test suites cover the body and ignore the plumbing.

10. Review After Every Architecture Change

Adding a proxy, a CDN, or a new backend changes the trust boundaries, so re-evaluate which headers are trusted and by whom every time the path changes.

Quick Reference: Header Injection Defense Checklist

Layer

Action

Edge

Reject control characters and malformed header values

Host

Enforce an allowlist and reject unknown hosts

URLs

Build links from configured values, not from requests

Forwarded headers

Strip client supplied versions, then set your own

Cache

Align cache keys with everything that changes output

Parsers

Keep front end and back end framing consistent

Code

Use structured header APIs, not string concatenation

Logging

Capture raw headers alongside parsed ones

Testing

Include header injection in regular test cycles

Architecture

Re-review trust boundaries after every change

The Bottom Line

Headers are instructions, and every component in the request path makes decisions based on what they say, which means the metadata layer is an execution layer that most teams never audit.

CRLF injection ends a header early and starts a new one, Host header attacks abuse a value that is user controlled but almost universally trusted, and proxy abuse exploits the disagreements between components that each believe they know what the request means.

The defenses are not exotic. Validate at the edge, allowlist the Host, stop building URLs from requests, strip and set forwarded headers yourself, align cache keys with output, and keep every parser reading the same way.

Test the plumbing, because that is where the attacker is looking, and nobody else is watching it.

FAQ

What is CRLF injection?

It is the injection of carriage return and line feed characters into a value that ends up in an HTTP header, which lets an attacker terminate the header early and add new headers or even a new response body.

What is HTTP response splitting?

It is the outcome of CRLF injection when the attacker injects enough separators to end the header block and write a complete second response, which can poison caches and deliver script to other users.

Why is the Host header dangerous?

Because it is user controlled and almost universally trusted, so applications that build links or make routing decisions from it can be manipulated into sending victims to attacker controlled destinations.

What is password reset poisoning?

It is an attack where the attacker triggers a password reset with a malicious Host header, so the victim receives a genuine email containing a reset link pointing at the attacker's domain, which delivers the token.

How do I stop my application from trusting forwarded headers?

Strip any client supplied version at the edge, then set the headers yourself with values you control, and never allow the application to read a forwarded header that a client could have written.

Should I disable caching entirely?

Not necessarily, but the cache key must include everything that changes the response, and if you cannot guarantee that, disabling caching for context dependent responses is the safer choice.

Professional Services

Explore Our Cybersecurity Services

Our insights are backed by hands-on service delivery. If your business needs professional cybersecurity support, our UK-based specialists are ready to help.

© 2016 – 2026 Red Secure Tech Ltd. Registered in England and Wales — Company No: 15581067