← Back to Research

From CRLF to Account Takeover: A 5-Step Chain We Found in 38 Companies

38 of 612 companies. 6.2%. Full account takeover, no password compromise. Five "low severity" findings that triage out individually combine into an end-to-end ATO chain — CRLF injection, cookie injection, session fixation, MFA-binding bypass, and persistent identity capture. Single-test scanners don't see it because no single test is the bug. The chain is the bug.

In February 2026, CelvexGroup's chain-test engine scanned 612 customer-authorized targets across our Series-B-and-up book. On 38 of them — 6.2% — the engine successfully demonstrated a five-step exploitation path from a CRLF-injection finding all the way to authenticated account takeover, without ever phishing a credential or compromising a password. Each individual link in the chain would have triaged at low or informational severity in a conventional scanner. The chain delivers full access to a victim account, including past the multi-factor challenge.

This post documents the chain, why traditional scanning missed it, the prevalence we observed in the field, and the specific patches that break the chain at each link. If you operate a customer-facing web application with cookie-based session management, this is worth your next 8 minutes.

What happened

The five-step chain, written as an attacker would execute it:

Step 1 — CRLF injection in a "harmless" reflection point

Find an HTTP response header that reflects user input from a query parameter, redirect target, or path component. Common loci: Location: headers from custom redirect endpoints, Content-Type overrides from file-download endpoints, Link: headers from API gateways. Confirm the input is reflected without sanitization of carriage-return / line-feed bytes. Severity in most scanners: low.

What it actually buys the attacker: the ability to inject arbitrary additional response headers into the victim's browser response, by encoding %0d%0a into the input.

Step 2 — Cookie injection via the CRLF primitive

Use the CRLF primitive to inject a Set-Cookie: header. The browser receives the response, sees a legitimate-looking Set-Cookie from the application's own origin, and stores the cookie. The attacker now controls a cookie value of their choice, on the victim's browser, scoped to the application's domain.

Crucially, this works even if the application's primary session cookie is marked HttpOnly and Secure — those flags prevent the attacker from reading the victim's existing cookie via JavaScript, but they do not prevent the browser from accepting a new cookie set by the server.

Step 3 — Session fixation

The injected cookie is the application's session identifier (JSESSIONID, laravel_session, connect.sid, or whichever framework convention the target uses). The attacker has pre-acquired this session ID by visiting the application themselves and capturing their own unauthenticated session. They inject that pre-acquired session ID into the victim's browser via the CRLF primitive.

The chain hinges on a specific, common mistake: when the victim subsequently authenticates, the application elevates the existing session to authenticated state rather than rotating the session ID at the privilege boundary. The attacker, who knows the session ID they injected, now also has an authenticated session.

OWASP has flagged session-ID rotation at authentication for over a decade. Most modern frameworks rotate by default. We still found unrotated sessions on 11.4% of the apps we scanned in February.

Step 4 — MFA-binding bypass

This is the step most defenders assume is impossible. The application requires MFA on login, so even if the attacker inherited the post-auth session, they should be blocked at the second-factor challenge.

In practice, the binding between MFA-completion state and session identity is implemented inconsistently. Three common patterns we observed make the bypass possible:

Step 5 — Persistent identity capture

The attacker now holds a session that the application treats as an authenticated, MFA-completed session for the victim user. They register a new MFA device through the standard "add a new authenticator" self-service flow — which most apps allow without re-prompting for the password. They then revoke the victim's original MFA device, and the takeover is complete and persistent. The victim is locked out; the attacker holds the account.

Why it kept working

Three reasons the chain stays exploitable across so many production targets:

Severity triage breaks the chain into invisible pieces. A bug-bounty triager looking at "CRLF injection in Location header" with no demonstrated impact files it as P5 informational and closes it. The same triager looking at "session ID is not rotated on login" files it as P4 low and notes that MFA mitigates the realistic impact. Looking at "MFA-completion state is stored as a session boolean" they file it as P3 medium with "requires session fixation to exploit" as a downgrade. Each triage decision is locally reasonable. Together they produce the situation where an end-to-end account takeover sits in three closed tickets across two quarters.

Single-test scanners do not chain. Conventional dynamic application security testing tools fire one payload at one endpoint and check for one signal. They are architecturally incapable of demonstrating "use the output of finding A as the precondition for finding B." Even the better commercial scanners that claim "chain detection" typically chain only across XSS and SQL injection variants — not across header injection plus session management plus MFA binding.

Frameworks deflect responsibility. Each link in the chain has a "the framework should have handled this" defense. CRLF? "The web server should sanitize." Session fixation? "Express/Spring/Rails rotate by default." MFA binding? "The auth library handles it." The defenses are partially true, which is worse than fully false — they create a confidence that the chain cannot exist while leaving the specific configurations that enable it widely deployed.

What we found in 612 companies

Prevalence data from the February 2026 scan, by chain step:

StepPrevalenceCompanies affected
Step 1: CRLF reflection in a response header21.7%133 / 612
Step 2: Reflection allows Set-Cookie injection14.2%87 / 612
Step 3: Session ID not rotated at authentication11.4%70 / 612
Step 4: MFA-completion state inherits via fixation9.0%55 / 612
Step 5: New-authenticator add does not require password re-prompt7.8%48 / 612
All five conditions present (full chain)6.2%38 / 612

Per-step prevalence is roughly multiplicative as expected for partially independent conditions. The 6.2% full-chain prevalence is the actionable number — that is the percentage of well-funded, security-aware, mostly-Series-B-or-later companies whose customer-facing web applications were one targeted exploitation campaign away from no-password account takeover.

What to check today

Five concrete checks, one per chain step. Each is locally testable; you do not need a chain-aware scanner to validate them.

  1. Audit response-header reflection. Identify every endpoint that emits a header containing user-influenced data. Confirm that the framework or upstream proxy strips %0d, %0a, and Unicode equivalents (\u000d, \u000a). Test by sending ?next=https://example.com%0d%0aSet-Cookie:%20test=1 against each suspect endpoint and inspecting the response with curl -i.
  2. Verify session-ID rotation at the privilege boundary. Acquire an unauthenticated session ID. Authenticate. Confirm the post-auth session ID is different from the pre-auth one. If your framework documents this as default-on, prove it experimentally — defaults change between versions and a load-balancer or session-affinity layer can override the rotation.
  3. Re-derive MFA state per-request from a signed token. Do not store mfa_completed = true on the session. Issue a short-lived, audience-bound, signed token at MFA completion that includes the session ID, the user ID, and an expiry. Validate all three on every privileged request.
  4. Bind remembered-device cookies to user-agent and IP fingerprint. The cookie that says "don't prompt for MFA on this browser" should be useless if presented from a different browser or a wildly different network. Lock the cookie to the user-agent and a network-fingerprint hash; treat any drift as a signal to re-prompt.
  5. Require password re-prompt for any change to authentication factors. Adding an authenticator, removing an authenticator, changing recovery codes, or modifying email-of-record must require password re-entry within the last 5 minutes. This single control breaks the persistent-capture step regardless of how the rest of the chain plays out.

How CELVEX Group tests for this

The chain-test framework is the part of our scanner that conventional tools do not have. Single-test logic answers the question "does payload X trigger signal Y at endpoint Z." Chain-test logic answers a different question: "does the output state of finding A satisfy the precondition state of finding B, and does that composition produce an exploitable outcome the customer cares about."

The specific test ID for this five-step CRLF-to-ATO pattern is CHAIN-ATO-CRLF-FIVE-STEP-001, and the implementation lives in core/test_catalog/_supplement_chains_2026-03.py. The orchestration logic:

The chain-test architecture is the reason CelvexGroup customers consistently uncover exploitation paths their pen-test reports and bug-bounty queues had been holding as separate, low-severity items for quarters at a time. The bug is the chain. You cannot find chains with tools that do not chain.

Bottom line

Five low-severity findings, none of them P1 alone, combine into account takeover with no password compromise on 38 of 612 companies in our February sweep. The defenses are mostly known, mostly documented, and mostly partially implemented — which is the worst place to be. Each of the five checks above is something a development team can verify before next sprint review. The chain breaks at any single link. Pick one, fix it cleanly, and you are out of the 6.2%.

Pen-testers hand you a PDF once a year. Single-test scanners hand you a list of low-sev findings to ignore. CelvexGroup's chain engine demonstrates the path from low-sev to "stop everything" — every week, with the proof attached.

Sources

Run a free Exposure Check — 60 seconds, no signup

See the publicly visible signals an attacker would use to test your application for the first link in this chain. No account required.

Start your Exposure Check