How XSS Vulnerabilities Are Introduced Through Modern JavaScript Frameworks

Modern frameworks like React, Vue, and Angular escape output by default, which has genuinely reduced how often classic XSS shows up in framework-built applications. It has not eliminated it, it has mostly relocated it to a smaller set of specific, well-documented escape hatches developers reach for under deadline pressure.

Modern frameworks close most of the old doors. A handful of new, deliberate ones remain, clearly labeled but easy to miss under deadline pressure.

In this post

  1. Why classic XSS became rarer with modern frameworks
  2. The dangerouslySetInnerHTML pattern, and its equivalents
  3. Server-side rendering and hydration mismatches
  4. Third-party components and dependency-introduced XSS
  5. URL and attribute-based injection points frameworks do not fully cover
  6. Practical mitigation beyond "the framework handles it"

Cross-site scripting has not gone away with the rise of component-based JavaScript frameworks, but its typical entry point has genuinely shifted. Understanding where framework-based applications remain vulnerable requires understanding specifically what these frameworks protect by default, and where developers still have to opt back into risk deliberately, even if not always knowingly.

Why classic XSS became rarer with modern frameworks

Frameworks like React, Vue, and Angular escape dynamic content by default when rendering it into the DOM through their standard templating or JSX syntax. A value inserted through normal binding, {userInput} in React or {{ userInput }} in Vue, gets automatically HTML-escaped before insertion, meaning a string like <script>alert(1)</script> renders as inert visible text rather than executing as a script. This default behavior alone closes off a large share of the classic, straightforward reflected and stored XSS patterns that plagued earlier, template-string-based server rendering approaches, where developers had to remember to escape output manually on every single interpolation.

The dangerouslySetInnerHTML pattern, and its equivalents

Every major framework provides an explicit escape hatch for cases where a developer genuinely needs to render raw HTML, a rich text editor's output, for instance. React calls this dangerouslySetInnerHTML, a name deliberately chosen to signal danger. Vue offers v-html. Angular provides [innerHTML] combined with its sanitization pipeline, which can be bypassed through bypassSecurityTrustHtml. Each of these exists for legitimate use cases, and each one disables the framework's default escaping for that specific piece of content.

// React example, bypassing default escaping
<div dangerouslySetInnerHTML={{ __html: userSuppliedContent }} />

The vulnerability is not the existence of this feature, it is any case where the content passed into it originates from user input, whether directly or after passing through some processing step the developer assumed was sufficient sanitization but was not. This pattern, an escape hatch used with insufficiently sanitized user-influenced content, accounts for a significant share of the XSS findings that still show up in modern framework-based applications during security review.

Server-side rendering and hydration mismatches

Applications using server-side rendering, common with frameworks like Next.js or Nuxt, introduce a more subtle risk surface. Content rendered on the server and later hydrated on the client can, in specific misconfigurations, create a window where server-rendered HTML containing insufficiently sanitized data gets sent to the browser before the client-side framework's protections are fully active. This is a narrower, more implementation-specific risk than the dangerouslySetInnerHTML pattern, but it has shown up in real, documented vulnerability disclosures against specific SSR framework versions, which is part of why keeping these frameworks updated matters beyond general feature improvements.

Third-party components and dependency-introduced XSS

A large share of real-world framework applications lean heavily on third-party component libraries, rich text editors, markdown renderers, chart libraries that accept custom HTML labels. Each of these is a piece of code outside the core framework's own escaping guarantees, and each one carries its own, independent responsibility for safely handling any user-influenced data passed into it. A vulnerability in a popular, widely used component library can suddenly affect every application depending on it, which is precisely the pattern behind several notable disclosed vulnerabilities in popular npm packages over the years, discoverable and trackable through resources like the GitHub Advisory Database for anyone auditing their own dependency tree.

URL and attribute-based injection points frameworks do not fully cover

Framework default escaping generally protects text content and standard attribute values well, but certain attribute contexts remain a known risk if user input flows into them without additional validation, most notably href and src attributes, where a value like javascript:alert(1) can execute even though the framework correctly escaped the string as an attribute value, since the danger here is the URL scheme itself, not HTML injection in the traditional sense. Frameworks have increasingly added specific protections against this pattern, but it remains a documented category worth explicit validation whenever user-controlled data can influence a link's destination.

Why "the framework handles it" is an incomplete mental model: frameworks handle a large, meaningful share of the classic XSS surface by default, and that is a genuine, significant improvement over older approaches. Treating that default protection as comprehensive, rather than understanding its specific boundaries, is exactly how the escape-hatch and dependency-introduced categories above keep showing up in otherwise modern, well-built applications.

Practical mitigation beyond "the framework handles it"

  • Audit every use of dangerouslySetInnerHTML, v-html, and equivalent escape hatches in a codebase specifically, treating each one as requiring explicit, deliberate justification and sanitization, not a routine pattern
  • Use a well-maintained sanitization library like DOMPurify for any case where raw HTML rendering is genuinely necessary, rather than a custom or partial sanitization approach
  • Deploy a Content Security Policy, covered in more depth in an earlier post on this blog, as a defense-in-depth backstop that limits the damage even if a sanitization gap is missed somewhere in the application
  • Audit third-party component and dependency usage specifically for known vulnerabilities, using automated tooling integrated into a CI pipeline rather than a one-time manual check
  • Explicitly validate URL schemes on any user-influenced link destination, rather than relying solely on the framework's general attribute escaping to cover this specific case

Modern frameworks have meaningfully changed the shape of XSS risk without eliminating the underlying vulnerability class. The classic, careless "forgot to escape output" pattern has become rarer, replaced by a smaller number of more specific, more deliberate risk points, escape hatches, third-party components, and edge-case attribute contexts, that require the same underlying discipline the frameworks' defaults were built to reduce the need for everywhere else.