A practical developer's guide to understanding how XSS works, the difference between Reflected, Stored, and DOM-based XSS, what attackers can do with it, and how to prevent it.
Cross-Site Scripting (XSS) is a web security vulnerability that occurs when an application handles untrusted data in a way that allows the browser to interpret that data as executable content. The fundamental problem is a failure to maintain a clear separation between data and code.
XSS can occur when user-controlled or otherwise untrusted data is inserted into HTML, JavaScript, CSS, URLs, or the DOM without the appropriate security controls. If an attacker successfully causes JavaScript to execute in the security context of a vulnerable website, that script may be able to interact with the application as the victim.
XSS is not limited to <script> tags. Depending on the injection context, attackers may abuse HTML elements, event-handler attributes, dangerous URL schemes, DOM APIs, or other browser features.
The impact of XSS depends on the application's functionality, the victim's privileges, browser protections, and other security controls. A successful XSS vulnerability can allow an attacker to:
HttpOnly attribute cannot be read directly through JavaScript.Importantly, HttpOnly cookies do not make XSS harmless. Although JavaScript cannot directly read an HttpOnly cookie, malicious JavaScript executing in the victim's browser may still be able to make authenticated requests to the vulnerable application using the victim's existing session.
The exact impact depends on the application's authorization model, available endpoints, browser security controls, Content Security Policy (CSP), cookie configuration, and other defenses.
XSS is commonly categorized as Reflected XSS, Stored XSS, and DOM-based XSS.
The first two describe how the malicious input is delivered and processed by the application. DOM-based XSS specifically describes vulnerabilities where unsafe processing occurs in client-side JavaScript and the malicious data does not necessarily need to be processed by the server.
Reflected XSS occurs when malicious input from an HTTP request is immediately included in the application's response and interpreted by the victim's browser as executable content.
Consider a vulnerable search page:
<p>You searched for: Hello</p>Suppose the application takes the query parameter from the URL and inserts it into the HTML without appropriate output encoding.
A malicious request could contain input such as:
https://example.com/search?query=<script>alert('XSS')</script>If the application reflects that value into an unsafe HTML context, the browser may interpret the injected markup as code.
A simplified vulnerable response could become:
<p>You searched for: <script>alert('XSS')</script></p>The attack generally requires the victim to make a request containing the malicious input, often by visiting a crafted URL. The malicious URL could be delivered through email, messaging, a website, a redirect, or another mechanism.
Reflected XSS is not inherently less serious than Stored XSS. Its impact depends on where the vulnerable functionality exists, who can be targeted, and what the injected JavaScript can access.
Stored XSS occurs when attacker-controlled input is stored by an application and later rendered to other users without appropriate security controls.
Common locations include:
For example, imagine a website allowing users to submit comments. If the application stores the submitted content and later places it directly into an HTML page without safe handling, malicious content could execute whenever another user views the affected comment.
Unlike reflected XSS, the payload does not necessarily have to be included in a malicious URL for every victim. Once stored successfully, it can execute when users load the affected content.
Stored XSS can be particularly serious when the affected page is regularly visited by administrators or other privileged users.
An attacker submits malicious content through a vulnerable comment form.
The application stores the content:
Attacker-controlled contentLater, the application generates:
<div class="comment">
Attacker-controlled content
</div>If the content is interpreted as HTML rather than safely rendered as text, the browser may execute attacker-controlled code.
The correct defense is not simply to remove a few suspicious strings. The application should safely handle the data according to the context in which it is rendered.
DOM-based XSS occurs when client-side JavaScript takes attacker-controlled data and uses it in an unsafe DOM operation.
In this case, the server may return a completely harmless response. The vulnerability exists in the browser-side code.
For example, consider this client-side script:
<script>
var name = new URL(location.href).searchParams.get('name');
document.getElementById('welcome').innerHTML = "Welcome, " + name;
</script>The JavaScript reads the name parameter and places it into the page using innerHTML.
Because innerHTML parses its input as HTML, using attacker-controlled data here can create an XSS vulnerability.
A safer implementation would be:
<script>
const name = new URL(location.href).searchParams.get('name');
document.getElementById('welcome').textContent = "Welcome, " + name;
</script>textContent treats the value as text rather than parsing it as HTML.
DOM-based XSS can also involve the URL fragment:
https://example.com/welcome.html#name=...The fragment after # is normally not included in the HTTP request sent to the server, but client-side JavaScript can access it through browser APIs.
Therefore, an application can have a DOM-based XSS vulnerability even when the server never receives the malicious value.
The key issue is not whether the server receives the payload. The key issue is whether client-side code takes attacker-controlled data and places it into a dangerous execution context.
A useful way for developers to understand DOM-based XSS is to think in terms of sources and sinks.
A source is a location from which attacker-controlled data can originate.
Examples include:
location.hreflocation.searchlocation.hashdocument.referrerpostMessage dataA sink is a function or API that interprets or inserts the data in a potentially dangerous way.
Examples include:
innerHTMLouterHTMLinsertAdjacentHTML()document.write()eval()setTimeout()Not every use of these APIs is automatically vulnerable. The security risk depends on whether attacker-controlled data can reach the sink and how the data is interpreted.
There is no single defense that solves every XSS problem. The most effective approach combines safe coding practices, contextual output handling, framework protections, secure DOM APIs, appropriate validation, and defense-in-depth controls.
One of the most important XSS defenses is context-aware output encoding.
The correct encoding method depends on where the untrusted data is being inserted.
HTML text, HTML attributes, JavaScript, CSS, and URL contexts have different parsing rules. Encoding data for one context does not automatically make it safe for another.
For example, if untrusted data needs to be displayed as ordinary HTML text, characters such as <, >, and & need to be safely represented so the browser treats them as text rather than markup.
For example:
<script>is displayed as text rather than interpreted as an actual <script> element.
This is a critical rule:
Encode data for the context in which it is being used.
Do not take HTML encoding and assume it is automatically safe inside JavaScript, CSS, or URLs.
Whenever possible, avoid placing untrusted data directly inside executable contexts.
Modern web frameworks often provide automatic HTML escaping for normal template output.
For example, many template engines automatically escape variables when they are rendered into HTML.
Developers should understand their framework's escaping behavior and avoid disabling it unless there is a specific, well-understood reason.
Dangerous patterns often appear when developers intentionally bypass normal escaping mechanisms to render raw HTML.
If raw HTML is genuinely required, the application should use a carefully configured HTML sanitizer rather than blindly trusting the input.
textContent Over innerHTMLWhen inserting untrusted text into the DOM, prefer:
element.textContent = userInput;instead of:
element.innerHTML = userInput;textContent treats the value as text.
innerHTML parses the value as HTML and therefore requires significantly more care.
Other DOM APIs that insert or interpret HTML should also be treated carefully, including:
element.outerHTMLelement.insertAdjacentHTML()document.write()The goal is to use APIs that treat untrusted values as data rather than executable markup.