Second-Order XSS: Hunting Payloads That Execute Later
Second-order XSS breaks the usual test rhythm. You submit input to endpoint A, nothing happens, and the payload executes hours later in endpoint B after the value has crossed a database, queue, or transformation job. Scanners miss it because they look for an immediate reflection. Hunters find it by following data.
Think of every editable field as a source: name, organization, shipping address, filename, calendar title, webhook label, imported CSV cell, support message. Then list its sinks. Does the value show up in an admin console, invoice preview, team activity feed, moderation queue, PDF viewer, notification toast, or analytics dashboard? The farther a value travels, the more likely one renderer forgot contextual encoding.
Use a distinct marker per field, such as BF19-COMPANY-A7 and BF19-FILE-B4. Browse all pages available to your own accounts and search responses for those markers. Trigger lifecycle events: invite a second account, export a report, cancel an order, open a ticket, and change roles. This mapping work feels slow, but it exposes sinks that blind payload lists never reach.
Prove interpretation gently
When a marker appears, inspect the surrounding HTML or script. If it lands between tags, test a harmless element. If it lands inside an attribute, choose an attribute-context probe. JSON embedded in a script block needs JavaScript-aware escaping, not generic HTML entities. The label “XSS” describes execution, not a particular string.
Keep the first execution self-contained: log a unique message, alter a harmless element, or set a flag in local storage for your own session. Avoid cookie exfiltration and external script hosting unless the program explicitly requires it. A delayed alert(1) may be cliché, but it is safer than sending privileged data to your server.
Second-order bugs often gain severity through role crossing. A customer controls an address; finance staff open an invoice; script runs in a back-office origin. Or a low-privilege team member names a project; an owner views the audit page. Demonstrate with two roles you own whenever possible. Never poison a shared queue and wait for an unsuspecting employee.
Transformations create surprising payloads
Data may be safe at entry and dangerous after processing. Markdown becomes HTML. CSV becomes a table. A URL decoder runs twice. An API stores escaped text, then a migration decodes it before rendering. Test the actual pipeline with paired canaries rather than assuming the first sanitizer settled the issue.
Watch for values copied between records. A sanitized profile name may become an unsanitized invoice customer name. An imported file's metadata may be inserted into a DOM template with innerHTML. Background jobs can execute after a delay, so timestamp submissions and revisit generated artifacts.
Explain the two stages
Your report should make the delay easy to reproduce. Stage one stores the payload: include the request and field. Stage two triggers execution: include the route, role, wait condition, and browser context. Show the raw stored value and the final rendered snippet if possible. Triage should not have to guess which job connects the two.
Recommend context-specific output encoding at every sink, even if input was sanitized earlier. Replace unsafe DOM APIs such as innerHTML with text-based rendering where markup is unnecessary, and sanitize rich HTML immediately before rendering with a maintained library. Most of all, test internal tools with hostile customer data. Trust changes as data moves; its bytes do not.