DEV Community

Cover image for What Is Cross-Site Scripting (XSS)? Understanding a Critical Web Security Vulnerability.
Aditya Sharma
Aditya Sharma

Posted on

What Is Cross-Site Scripting (XSS)? Understanding a Critical Web Security Vulnerability.

Imagine a website where users can post comments. Someone submits this as their comment:

<script>
    alert("Hello");
</script>
Enter fullscreen mode Exit fullscreen mode

If the application takes that input and places it directly into the HTML it serves to other users, the browser doesn't see a comment. It sees a script tag. The vulnerability isn't that JavaScript exists on the page. JavaScript belongs on web pages. The problem is that untrusted user input ended up in a context where the browser interpreted it as executable content rather than inert text.


The Core Problem

A browser rendering a webpage doesn't distinguish between HTML the developer wrote and HTML that arrived through a comment field. It parses what it's given. If user input gets embedded into the page without being handled carefully, the browser processes it the same way it processes everything else.

User Input
    ↓
Web Application
    ↓
HTML / DOM
    ↓
Browser
    ↓
Input interpreted as executable content
Enter fullscreen mode Exit fullscreen mode

Untrusted data should remain data. XSS occurs when the application allows that data to cross into a context where the browser interprets it as code or executable markup. The boundary between "string containing angle brackets" and "HTML the browser will parse" is where the vulnerability lives.


Three Forms of XSS

XSS shows up in a few different ways depending on where the injection happens and how the input travels.

Stored XSS is when untrusted input gets saved to a database and later served to other users. The comment example above is stored XSS. An attacker submits input once, and every user who views that page subsequently receives it. The application acts as an unwitting distribution mechanism.

Reflected XSS involves input that isn't stored but gets reflected back in an immediate server response. Search pages are a common example: if a query is echoed into the page as "You searched for: [query]" and the query isn't handled carefully, an attacker can craft a URL whose query parameter contains a payload. When another user visits that URL, the server reflects the input back in the response without the browser having any way to know it didn't come from the developer.

DOM-based XSS is different in that no server-side injection is required. The server sends a perfectly safe response, but client-side JavaScript reads from an attacker-controlled source, such as a URL fragment or a query parameter, and places it into an unsafe DOM context without appropriate handling. The server does not need to store or reflect the payload; the vulnerability exists entirely in how the client-side code processes data.


Why innerHTML Matters

This is where the mechanism becomes most concrete for web developers.

element.innerHTML = userInput;
Enter fullscreen mode Exit fullscreen mode

When you assign to innerHTML, you're asking the browser to parse the string as HTML. Attacker-controlled markup can therefore create elements or attributes that introduce executable browser contexts. An image element with an onerror handler, for example, doesn't require a <script> tag at all:

element.innerHTML = '<img src="x" onerror="/* runs here */">';
Enter fullscreen mode Exit fullscreen mode

The string has become markup, and the browser treats it accordingly.

element.textContent = userInput;
Enter fullscreen mode Exit fullscreen mode

textContent is different. It assigns the string as plain text. Angle brackets are treated as literal characters, not HTML. Whatever the user submitted appears on the page as written, not as parsed markup.

The distinction isn't that innerHTML is inherently dangerous. innerHTML is a legitimate API, and assigning developer-controlled or appropriately processed content through it is fine. The risk arises specifically when untrusted input flows into it without being handled: the browser cannot tell that the string originated from a user rather than the application, so it parses it all the same.


Frameworks and Where XSS Still Appears

Modern frontend frameworks generally escape interpolated content by default. When you render a variable in a React component or an Angular template, the framework treats it as text rather than HTML, preventing ordinary variables from being interpreted as markup.

But frameworks provide explicit ways to opt out of this. React has dangerouslySetInnerHTML. Angular has bypassSecurityTrustHtml. These APIs exist for legitimate use cases where rendering HTML is genuinely necessary. When developers use them with untrusted input, they've deliberately stepped outside the safe defaults the framework was providing.

The important point is that frameworks reduce the surface area for XSS by making the safe path the default path, but they don't eliminate the possibility. Developers can still create unsafe data flows when they work around framework protections, or when they use lower-level DOM APIs directly.


Defenses

Output encoding is the primary defense. When user-supplied data is placed into HTML, JavaScript, CSS, or a URL, it should be encoded appropriately for that context so the browser treats it as data rather than code. The encoding rules differ by context: what's safe to leave unescaped inside an HTML attribute is different from what's safe inside a JavaScript string. Treating all output encoding as a single uniform operation misses this.

Safe DOM APIs are the practical expression of this for client-side code. textContent is safe for plain text because it doesn't invoke HTML parsing. Building DOM nodes programmatically with createElement and setting their properties explicitly avoids the risk of parsing an arbitrary HTML string, though safety still depends on what's assigned to those properties afterward.

Framework-provided escaping is useful because it pushes the safe default into the rendering layer, so developers get protection without having to remember to apply encoding manually on every interpolated value.

Content Security Policy is defense in depth. A strong CSP can tell the browser which scripts are permitted to run, limiting the impact of a successful injection. It doesn't remove the underlying vulnerability; if untrusted input is making it into an executable context, CSP is constraining the damage rather than preventing the injection. It's a meaningful layer, but not a substitute for fixing the root cause.

Input validation is worth doing for many reasons, but it shouldn't be treated as the primary XSS defense. Validating that a field looks like an expected format doesn't guarantee that whatever passes validation is safe to insert into every possible output context. An application that validates inputs but then places them into the page without encoding is still vulnerable.


The browser has no way to know where a string came from. It doesn't know a piece of HTML arrived through a comment field rather than from the developer. It just parses what the application puts in front of it.

XSS is not a browser flaw, and it's not a problem with JavaScript. It's a failure to maintain the boundary between data and code. The application accepted input, and somewhere between accepting it and rendering it, that input crossed into a context where the browser would execute it.

Keeping that from happening is the whole job.

Top comments (0)