Stored XSS in a Support Portal with a Strict CSP
You are testing a customer support portal at support.corp-example.com. Agents view tickets; customers create tickets and can see their own thread. Ticket subjects are rendered with element.innerHTML after a JSON fetch. The client-side sanitizer allows img, svg, a, and iframe for embedded videos, but strips script tags, on* attributes, and javascript: URLs. The subject field is not sanitized. CSP is: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'; connect-src 'self'. Session cookies are HttpOnly and SameSite=Lax. You find a legacy same-origin JSONP endpoint, /api/v1/jsonp?callback=, that reflects the callback parameter without validation. The portal also has /api/tickets/<id>/internal-notes, accessible to agents only. A customer can create unlimited tickets and view their own ticket thread.
1. Is the stored subject injection exploitable given the CSP? Construct a payload chain or explain why not.
Read the worked answer
A naive payload fails. Injecting <script>alert(1)</script> through innerHTML does not execute because script elements inserted by innerHTML are inert. Injecting <img src=x onerror=alert(1)> is blocked because CSP script-src lacks 'unsafe-inline', so inline event handlers do not run. A javascript: link is also governed by script-src and is blocked. The presence of the nonce does not help an attacker, and base-uri 'none' prevents base-tag tricks.
The chain is the sanitizer allowlist plus the same-origin JSONP endpoint. The sanitizer permits iframe, and CSP default-src 'self' permits a same-origin iframe. Inject a subject such as:
<iframe srcdoc="<script src='/api/v1/jsonp?callback=alert(parent.document.domain)//'></script>"></iframe>
The srcdoc document inherits the parent origin and the CSP. Its script tag loads a same-origin URL, which script-src 'self' allows. The JSONP endpoint reflects the callback, so the response begins with alert(parent.document.domain)// followed by the JSONP body. That executes. The nonce is irrelevant because this is an external same-origin script, not an inline script. The finding is exploitable, but it depends on the iframe allowlist and the JSONP gadget; if either is removed, the same sink may not execute without another script gadget.
2. Assuming execution, how would you exfiltrate an agent's internal notes despite HttpOnly cookies, SameSite=Lax, and connect-src 'self'? Explain the data flow and limits.
Read the worked answer
HttpOnly prevents JavaScript from reading document.cookie, but it does not prevent same-origin fetch from sending the cookie automatically. SameSite=Lax does not block a request initiated from support.corp-example.com to support.corp-example.com; it is same-site. CSP connect-src 'self' allows fetch to same-origin APIs, which is exactly what is needed.
The attacker first creates a ticket and records its ID. The payload runs in an agent's browser when the agent opens the malicious ticket. It calls fetch('/api/tickets/<current-id>/internal-notes') or an endpoint that lists tickets and notes, using the agent's session. It then writes the stolen text into the attacker's ticket through a same-origin write endpoint, for example POST /api/tickets/<attacker-ticket-id>/replies with the note text as the body. The attacker opens their own ticket thread and reads the reply. No external network access is required, so connect-src 'self' is not a barrier. If the write endpoint requires a CSRF token, the payload can read it from the current page because it is same-origin.
The limits are real: the agent must view the malicious ticket; the payload must fit in the subject or another stored field; the API must allow the agent to read notes and write to the attacker's ticket; and the exfiltration channel must be visible to the customer. If customers cannot see replies to their own tickets, use another same-origin channel such as creating a new ticket with the data in the subject, provided the attacker can view it.
3. What remediation would you prioritize and why?
Read the worked answer
Prioritize the JSONP endpoint first. It is a script gadget that converts an otherwise mitigated HTML injection into executable script under a CSP that trusts 'self'. Remove the endpoint or restrict callback to a strict allowlist of identifier names; do not merely filter obvious dangerous strings. Next, sanitize the subject with context-aware output encoding and stop rendering it with innerHTML. Treat the subject as text, or sanitize it with a maintained library and a narrow allowlist.
Then reduce the HTML allowlist: remove iframe unless a specific, sandboxed embed feature requires it. If iframes are required, use a separate sandboxed origin and a restrictive sandbox attribute. Tighten CSP toward nonce- or hash-based script loading with 'strict-dynamic' and avoid script-src 'self' where possible, because 'self' is what allowed the JSONP load. Add Trusted Types to block DOM XSS sinks. Enable a CSP report-only rollout to catch breakage. The order matters: fixing only the subject sanitizer leaves the JSONP gadget available for other injections; fixing only JSONP leaves the subject sink exploitable if another gadget appears.