Understanding XSS Attacks in Modern Web Apps
March 25, 2026
Cross-Site Scripting (XSS) happens when untrusted input is interpreted as executable code in a browser. If attackers can inject script into a page, they may steal session data, perform actions on behalf of users, or alter UI to phish credentials.
This article is inspired by Vercel security guidance and translated into practical React/Next.js implementation patterns.
1. How an XSS attack usually unfolds
A common attack chain:
- Attacker finds a place where user input is rendered unsafely.
- Attacker injects a malicious payload.
- Victim opens the affected page.
- Payload executes in victim browser under your site's origin.
- Sensitive data (token, session context) is exfiltrated or actions are triggered.
What makes XSS dangerous is that the code runs as if it belongs to your trusted app.
Deep dive: execution context is the core risk
When malicious JavaScript executes under your origin, browser security boundaries work for the attacker, not for you. The script can often:
- read page-level data (including sensitive state rendered in HTML),
- call internal APIs as the current user,
- trigger actions in the UI invisibly,
- rewrite forms or modals to harvest credentials.
This is why XSS often escalates from "just one vulnerable field" to account-level compromise.
XSS ATTACK CHAIN
2. Main XSS categories you need to know
DOM-based XSS
The vulnerability exists fully in frontend code, usually due to dangerous sinks such as innerHTML, outerHTML, insertAdjacentHTML, or unsafe URL handling.
Typical source-to-sink pattern:
- source:
location.search,location.hash, postMessage payload, or third-party widget data, - sink: DOM API that parses HTML/JS-sensitive content,
- result: browser executes attacker-controlled markup or script.
Reflected XSS
Payload comes from a request parameter and is reflected immediately in response HTML/JS context.
Reflected XSS is often used in phishing chains because the payload can be packed into a URL and distributed quickly.
Stored XSS
Payload is persisted (for example comment, profile, CMS field) and served to many users later. This is usually the most severe because it scales automatically.
Stored XSS is especially dangerous in admin-facing surfaces: if privileged users view infected content, the attacker may jump from one low-privilege account to full control.
XSS TYPES MATRIX
DOM XSS
Client-side sink misuse
Reflected XSS
Payload reflected in response
Stored XSS
Persistent payload from storage
3. Typical vulnerable patterns (and safer alternatives)
Anti-pattern
// Unsafe: attacker-controlled string interpreted as HTML
<div dangerouslySetInnerHTML={{ __html: userContent }} />Better pattern
// Safe by default: React escapes values rendered in JSX text context
<div>{userContent}</div>If rich HTML is truly required
import DOMPurify from "isomorphic-dompurify";
const sanitized = DOMPurify.sanitize(userContent);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;Rule of thumb: do not switch to raw HTML rendering unless business requirements force it.
Deep dive: context matters more than one sanitizer call
Sanitization helps, but context-aware output handling is still required:
- HTML context: escape/sanitize markup.
- Attribute context: validate protocols and allowed values.
- URL context: block
javascript:and malformed schemes. - Script context: avoid embedding untrusted values directly.
A lot of production XSS issues happen when teams sanitize once, then later reuse the same value in a different context where assumptions no longer hold.
4. Input validation: client helps UX, server enforces security
Client-side validation improves form experience, but it is not a trust boundary.
Server-side should always:
- validate schema and data type,
- normalize and constrain allowed formats,
- sanitize where HTML is accepted,
- encode output according to rendering context.
In production systems, this should happen at every write path, not only at UI forms.
Practical backend pattern
For user-generated content pipelines, split processing into stages:
- schema validation (shape/type),
- normalization (length, allowed tags/attributes),
- sanitization and persistence,
- safe rendering policy at read-time.
This avoids the "validated in one endpoint, bypassed in another" problem.
5. Cookie and session hardening against XSS impact
XSS prevention is primary defense, but blast-radius reduction is also essential:
- keep session cookies
HttpOnlyandSecure, - prefer
SameSite=LaxorStrictwhere possible, - avoid storing long-lived auth secrets in localStorage,
- rotate session and refresh tokens.
HttpOnly does not stop XSS itself, but it can prevent direct cookie reads via JavaScript.
Also remember: even with HttpOnly, attacker scripts may still perform authenticated actions from the victim browser. So cookie flags reduce impact but do not replace XSS prevention.
6. CSP is your containment layer
A strong Content Security Policy (CSP) can significantly reduce exploitability.
Useful principles:
- disallow inline scripts where possible,
- use nonce/hash-based policies for approved scripts,
- restrict script sources to trusted origins,
- monitor CSP violation reports.
CSP is not a replacement for output encoding and sanitization, but it limits damage when bugs slip through.
CSP deployment notes teams often miss
- Start with report-only mode to measure breakage.
- Move to enforcing mode with nonces/hashes.
- Keep policy close to build/runtime ownership so drift is visible.
- Treat new third-party scripts as a security review event.
XSS DEFENSE LAYERS
7. Practical checklist for React/Next.js teams
- Default to JSX escaping; avoid raw HTML rendering.
- Sanitize any user-generated HTML with a trusted sanitizer.
- Remove inline event handlers and inline script blocks.
- Validate and sanitize on server before persistence.
- Set
HttpOnly,Secure, and appropriateSameSitecookie attributes. - Add CSP with nonces/hashes and restricted script sources.
- Review third-party libraries/components that touch DOM directly.
- Add security tests for dangerous sinks (
innerHTML, URL injection paths).
8. Conclusion
XSS is still one of the most practical web attack vectors because it abuses trust in your own origin context. The strongest strategy is layered:
- strict input handling,
- safe output rendering,
- CSP and cookie hardening,
- secure defaults in component design.
If you implement these systematically, XSS moves from a recurring production risk to a controlled engineering concern.
Security maturity here is less about one-time fixes and more about engineering discipline: safe defaults, reviewable rendering boundaries, and continuous verification in CI and runtime.