October 9, 2026
$8,200 for Two Invisible Characters: CRLF Injection to Session Fixation
HTTP headers are separated from each other, and from the response body, by a carriage return followed by a line feed — two bytes, \r\n…

By T4nv1
3 min read
HTTP headers are separated from each other, and from the response body, by a carriage return followed by a line feed — two bytes, \r\n, that the protocol treats as pure structural syntax rather than content. If an application reflects user input into a header value without stripping those two bytes, an attacker doesn't just get to influence that one header's value. They get to inject entirely new headers, or even end the header section early and start writing the response body itself, because the application's reflection point has no concept of "this is supposed to be one header's value" beyond trusting there won't be a line break inside it.
The target, which I'll call Ferrowind here per their program's disclosure terms, is a content platform with a post-login redirect feature — after authenticating, users get redirected to whatever page they were trying to reach beforehand, passed through a returnUrl parameter.
The Reflection Point
GET /login?returnUrl=/dashboardGET /login?returnUrl=/dashboardA normal, successful login redirected via a Location header built directly from that parameter:
HTTP/1.1 302 Found
Location: /dashboardHTTP/1.1 302 Found
Location: /dashboardI tested whether the parameter was reflected with any sanitization at all by including a raw, URL-encoded CRLF sequence followed by an entirely new, arbitrary header:
GET /login?returnUrl=/dashboard%0d%0aX-Injected-Test:%20confirmedGET /login?returnUrl=/dashboard%0d%0aX-Injected-Test:%20confirmedThe response came back:
HTTP/1.1 302 Found
Location: /dashboard
X-Injected-Test: confirmedHTTP/1.1 302 Found
Location: /dashboard
X-Injected-Test: confirmedA header I'd never been meant to control had appeared in the response, fully formed, because the application concatenated my parameter value directly into the raw header block without stripping or encoding the line-break sequence I'd supplied. Confirmed: this reflection point was vulnerable to full header injection, not just value manipulation within a single existing header.
From an Arbitrary Header to Session Fixation
An injected, harmless test header is a real finding on its own, but the severity question is what a real header injection could actually achieve. The most consequential version of this technique against a reflection point like this is injecting a Set-Cookie header — forcing the victim's browser to adopt a session identifier the attacker already knows, a textbook session fixation setup.
GET /login?returnUrl=/dashboard%0d%0aSet-Cookie:%20sessionId=ATTACKER_KNOWN_VALUE_12345;%20Path=/GET /login?returnUrl=/dashboard%0d%0aSet-Cookie:%20sessionId=ATTACKER_KNOWN_VALUE_12345;%20Path=/The response reflected this cleanly as a genuine Set-Cookie header:
HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: sessionId=ATTACKER_KNOWN_VALUE_12345; Path=/HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: sessionId=ATTACKER_KNOWN_VALUE_12345; Path=/If Ferrowind's session-management backend would accept an attacker-chosen session identifier as valid once a real user later authenticated under it — rather than always generating a fresh identifier server-side at the point of login — this meant a victim who clicked a link containing this crafted URL, then logged in normally, would end up authenticated under a session ID I'd already set in advance. From that point, I wouldn't need to steal anything from the victim's browser at all; I'd simply use the session ID I'd chosen from the start, now elevated to a real, authenticated session the moment the victim completed their own login.
Confirming the Fixation Actually Worked
I tested this end-to-end using a browser logged into nothing, in a fully separate session from any of my own accounts. I loaded the crafted URL, confirmed via developer tools that sessionId=ATTACKER_KNOWN_VALUE_12345 was now set as a cookie in that browser, then completed a normal login using one of my own test accounts in that same browser. Afterward, from a second, entirely separate browser, I manually set the same cookie value and loaded Ferrowind's dashboard directly — and landed fully authenticated as my test account, with no credentials entered in that second browser at all. The fixation had worked exactly as intended: a session identifier I'd chosen before any authentication occurred became a fully valid, authenticated session the moment a real login happened to occur under it.
Scope
Both browser profiles throughout this test used only my own test account's credentials. I didn't distribute the crafted URL anywhere or attempt this against any session but my own.
The Report
Title: CRLF injection in returnUrl redirect parameter allows arbitrary header injection and session fixation via forced Set-Cookie
Root cause: The post-login redirect feature reflects the returnUrl query parameter directly into the response's raw header block without stripping or encoding carriage-return and line-feed sequences, allowing an attacker to terminate the intended Location header early and inject arbitrary additional headers, including Set-Cookie.
Reproduction: The injection-confirmation payload, the Set-Cookie-based fixation payload, and the full two-browser end-to-end demonstration confirming that a pre-chosen session identifier became validly authenticated after normal login — performed entirely with my own test account.
Impact: Any user who follows a crafted link can be fixated into a session identifier the attacker already knows, allowing the attacker to access the resulting authenticated session immediately after the victim logs in, without needing to intercept, steal, or guess any credential or token.
Fix recommendations:
- Strip or reject carriage-return and line-feed sequences from any user-supplied value before it reaches raw header construction, or use a response-building API that encodes header values safely by default rather than permitting raw string concatenation into the header block
- Always generate a fresh, cryptographically random session identifier server-side at the moment of successful authentication, regardless of whatever session cookie value a request already carried beforehand, which independently closes off session fixation even if a future header-injection bypass were found elsewhere
What Happened After
Ferrowind fixed the header-reflection sanitization within three days and additionally hardened session issuance to always regenerate the identifier at login, closing the fixation path through two independent layers. The report closed at $8,200, rated high severity.
Why CRLF Injection Rewards Checking Every Reflected Header
Any endpoint reflecting user input into a response header — redirects, custom headers, cache-control directives — deserves a raw CRLF test regardless of how innocuous the reflected value seems, because the consequence scales with whatever header an attacker manages to inject, and Set-Cookie specifically turns a cosmetic-looking bug into full session control.