Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin9/2/20269 min read

CSRF and XSS Are Still Breaking Production Apps — Here's the Current Defense Playbook

CSRF and XSS Are Still Breaking Production Apps — Here's the Current Defense Playbook

# CSRF and XSS Are Still Breaking Production Apps — Here's the Current Defense Playbook

TL;DR: A strict, nonce-based Content-Security-Policy plus SameSite=Lax (or Strict) cookies with Secure and HttpOnly closes most of the CSRF/XSS attack surface that hits production apps in 2026 — but neither one is sufficient alone, and newer transports like WebSockets slip past both if you don't validate origin explicitly.

Cross-site scripting and cross-site request forgery are two of the oldest bug classes in web security, and they are still landing CVEs with real CVSS scores against widely deployed software. In August 2026, a pre-authentication XSS in the WordPress core login screen (CVE-2026-64638, CVSS 8.9) showed that even a codebase audited for two decades can still have a parser differential that lets attacker-controlled markup reach the DOM. A few months earlier, CVE-2025-41254 showed that Spring's WebSocket/STOMP handling could be tricked into accepting unauthorized messages without a session — a CSRF-class bug in a transport that most teams don't think to protect with the usual token or SameSite defenses.

This is a practical, current playbook: what actually stops these two bug classes in 2026, with working configuration you can drop into a real app.

Why these two keep resurfacing

XSS and CSRF are old, but the attack surface keeps growing:

  • More client-rendered UI means more places where user input (usernames, comments, search queries, even file names) gets reflected into the DOM without going through a templating engine that auto-escapes.
  • More non-HTTP transports — WebSockets, Server-Sent Events, fetch with custom headers — don't automatically inherit the cookie and CORS protections browsers apply to normal form submissions and XHR.
  • Third-party scripts (analytics, chat widgets, ad tech) are a standing XSS risk because they run with the same DOM access as your own code.

Neither bug class is solved by a single header. They need layered controls.

XSS: the current playbook

1. Escape output by default

If you're on a modern framework (React, Vue, Next.js, Django templates, Rails ERB), you already get contextual auto-escaping for interpolated values. The exploitable spots are almost always where a developer deliberately opts out: dangerouslySetInnerHTML, v-html, {% autoescape off %}, .html() in jQuery, or building HTML strings by concatenation. Treat every one of those as a line that needs its own sanitization pass (e.g., DOMPurify) and a code review flag.

2. Ship a real Content-Security-Policy

A CSP is the single highest-leverage control against XSS because it stops injected <script> tags and inline event handlers from executing even if your escaping misses a spot. The weak version — script-src 'self' 'unsafe-inline' — defeats the point, since unsafe-inline lets any injected inline script run. The current recommended approach (per the MDN CSP guide and the OWASP CSP Cheat Sheet) is a strict, nonce-based policy:

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{RANDOM_PER_REQUEST}' 'strict-dynamic' https:;
  style-src 'self' 'nonce-{RANDOM_PER_REQUEST}';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'self';
  form-action 'self';
  upgrade-insecure-requests;

How this holds together:

  • 'nonce-{RANDOM_PER_REQUEST}' is a cryptographically random value generated fresh on the server for every response, embedded both in the header and as the nonce attribute on your legitimate <script> tags. An attacker's injected script won't carry the right nonce, so it won't execute.
  • 'strict-dynamic' tells the browser to trust scripts dynamically loaded by an already-trusted, nonced script (your bundler's chunk loader, for example), without needing to allowlist every CDN domain by name. This is what makes nonce-based CSP workable with modern bundlers that inject script tags at runtime.
  • object-src 'none' and base-uri 'none' close two classic CSP-bypass vectors (Flash/plugin injection and <base> tag hijacking).
  • form-action 'self' blocks a common CSRF-adjacent trick where an attacker uses injected content to redirect a legitimate form's submission to an external endpoint.

A minimal Express example for generating the nonce per request:

import crypto from "node:crypto";

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString("base64");
  res.locals.cspNonce = nonce;
  res.setHeader(
    "Content-Security-Policy",
    [
      "default-src 'self'",
      `script-src 'nonce-${nonce}' 'strict-dynamic' https:`,
      `style-src 'self' 'nonce-${nonce}'`,
      "object-src 'none'",
      "base-uri 'none'",
      "frame-ancestors 'self'",
      "form-action 'self'",
    ].join("; "),
  );
  next();
});

Ship it in Content-Security-Policy-Report-Only first against a report-uri/report-to endpoint, watch for violations from legitimate third-party scripts, then flip to enforcing.

Case in point: CVE-2026-64638 (WordPress "XSS2Shell")

Disclosed August 7, 2026, this pre-auth reflected XSS lived in wp-login.php. A crafted username submitted on a failed login attempt survived PHP's strip_tags() call but was later re-parsed as HTML by wp_kses_post() — a parser differential between two sanitization functions that individually looked correct. The resulting script ran in any visitor's browser with no authentication and no extra interaction on that page, and researchers published a chain escalating it to server-side PHP code execution when a logged-in administrator clicked an attacker link (The Hacker News, Hadrian). It's a useful reminder that a strict CSP would have stopped exploitation even though the sanitization bug itself existed at the framework level — defense in depth, not defense in one place.

CSRF: the current playbook

1. SameSite cookies as the default, not the exception

SameSite is a cookie attribute that tells the browser whether to send the cookie on cross-site requests. Chrome, Edge, and other Chromium browsers have treated cookies without an explicit SameSite value as Lax since Chrome 80, and that remains the Chromium default in 2026 (Chromium SameSite FAQ). Don't rely on the implicit default — set it explicitly, because Firefox and Safari don't uniformly apply the same default, and an explicit value documents the intent:

Set-Cookie: session=eyJhbGciOi...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=604800

Or in Express with cookie-session / express-session:

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    cookie: {
      httpOnly: true,
      secure: true, // requires HTTPS
      sameSite: "lax", // use "strict" for admin/internal apps with no cross-site entry links
      maxAge: 7 * 24 * 60 * 60 * 1000,
    },
  }),
);
  • SameSite=Lax blocks the cookie on cross-site POST/PUT/fetch requests (the classic CSRF vector: a hidden form on an attacker's page auto-submitting to your API) while still sending it on top-level navigation, so a user clicking a link into your app stays logged in.
  • SameSite=Strict is tighter — no cookie on any cross-site request, including top-level navigation — appropriate for admin panels or banking-style flows where you're fine asking the user to log in again after following an external link.
  • HttpOnly stops the cookie from being read via document.cookie, which is your backstop if an XSS bug does slip through — without it, a successful XSS becomes instant session theft instead of being limited to whatever the injected script can do live in the page.
  • Secure stops the cookie from ever being sent over plain HTTP.

2. SameSite is not a complete replacement for CSRF tokens

SameSite=Lax doesn't cover every case: subdomain-to-subdomain requests are same-site by the cookie spec even if they're different security boundaries in your architecture, and any request type your app treats as "safe" under Lax (simple top-level GET navigations) is still exempt. For state-changing endpoints, keep a synchronizer token (double-submit cookie or a server-issued CSRF token validated per request) as a second layer, particularly on anything that isn't a same-origin fetch/XHR call your own JS controls.

CVE-2025-41254 is the sharp example here: Spring's STOMP-over-WebSocket handling didn't require a session to be established before accepting messages, so an attacker page could get a victim's browser to open a WebSocket connection and send unauthorized STOMP frames — a CSRF-shaped bug that neither SameSite cookies nor a typical CSRF token would touch, because the WebSocket handshake doesn't go through the same request pipeline (spring.io advisory, HeroDevs writeup). The fix, shipped in Spring Framework 6.2.12, was explicit origin validation on the WebSocket handshake — the same principle as CSRF tokens, just applied to a transport that doesn't get it for free. If your app opens WebSocket or SSE connections, check that you're validating the Origin header server-side rather than assuming cookie-based auth protects you the way it does for regular HTTP requests.

Checklist

ControlStopsNotes
Contextual output escapingXSSDefault in most frameworks; audit every explicit opt-out
Nonce-based CSP with strict-dynamicXSSSet Report-Only first, then enforce
SameSite=Lax/Strict cookiesCSRFExplicit, don't rely on browser defaults
HttpOnly + Secure cookiesSession theft via XSSBackstop, not primary XSS defense
CSRF token (double-submit or synchronizer)CSRF on subdomain/edge casesStill needed alongside SameSite
Origin validation on WebSocket/SSE handshakesCSRF on non-HTTP transportsEasy to miss; not covered by cookie attributes

Getting this configuration right once, and keeping it enforced as the app grows, is exactly the kind of gap a periodic external security review catches before an attacker does — worth a look if your last CSP or cookie audit predates your current codebase, which is the kind of review Wise Hustlers' cybersecurity practice does for client applications.

FAQ

Does a Content-Security-Policy alone stop XSS?

No. CSP is a strong second layer against script execution, but it doesn't stop DOM-based data leaks, unsafe-inline/unsafe-eval misconfigurations, or bugs that inject content that isn't a script (like a malicious <img> that exfiltrates data via its src). Escape output first; use CSP as defense in depth.

Is `SameSite=Lax` enough to stop CSRF without a token?

For most typical apps it stops the majority of classic CSRF (cross-site form auto-submission), but it doesn't cover same-site subdomain trust boundaries or non-cookie-based transports like WebSockets. Keep a CSRF token on sensitive state-changing endpoints if your app has either of those.

Why did WordPress's own sanitization functions fail to stop CVE-2026-64638?

Because strip_tags() and wp_kses_post() parse HTML slightly differently — a "parser differential." Input that looked safe to the first function was later re-interpreted as HTML by the second, letting attacker markup through. It's a reminder that sanitization functions from the same codebase can still disagree with each other.

Do I need both CSP and SameSite cookies, or just one?

Both — they defend different failure modes. CSP limits what an XSS bug can do once it exists; SameSite (plus tokens) stops a forged cross-site request from using a valid session in the first place. Neither substitutes for the other.

Sources