Cross-Site Scripting (XSS) is a web security vulnerability enabling an attacker to inject malicious code into a page visited by other users. This vulnerability can compromise the confidentiality, integrity and security of the users and systems concerned.
What is an XSS vulnerability?
An XSS flaw occurs when a web application inserts unfiltered or unencrypted user-manipulable content into an HTML page. This content may include JavaScript code which is then executed in the user's browser. There are three main types of XSS:
- XSS Reflected: injection via a parameter transmitted on the fly.
- XSS Stored: injection recorded on the server side (e.g. database).
- XSS DOM-Based: injection exploiting client-side DOM modification by JavaScript.
Generally speaking, XSS allows you to do everything that is possible with JavaScript in the context of the web page. This includes stealing cookies (if they don't have the HTTPOnly flag), redirecting to other sites, displaying warning messages, inserting a fake login form, stealing accounts, and so on.
XSS Reflected
This type of attack is based on the injection of code into the URL or a request, which is immediately reused by the web page without any secure processing or storage.
This type of attack therefore often requires user interaction, for example by clicking on a link containing the malicious code (or payload).
Example of a vulnerable URL:
http://example.com/search?q=<script>alert(1)</script>
Vulnerable code:
<?php
if (isset($_GET['q'])) {
echo '<p>Search results for ' . $_GET['q'] . '</p>';
}
?>
Why it's vulnerable: The content of $_GET['q']
is inserted directly into the HTML without being encoded. If the user injects a <script>
tag, which allows JavaScript code to be inserted directly into the page, it will be executed by the browser.
Stored XSS
Here the malicious code is stored persistently (database, file, etc.) and will be executed each time the relevant page is displayed.
For example, if a blog's comments are vulnerable, an attacker could post a comment containing JavaScript code. Each time a user views a page containing this comment, the code will be executed in their browser.
Example of a malicious request:
POST /post/comment?id=1 HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
message=<script>alert(1)</script>
Vulnerable code:
// Display comment
foreach ($comments as $comment) {
echo '<p>' . $comment->message . '</p>';
}
Why it's vulnerable: The comment is displayed without HTML encoding. A <script>
tag injected into the message
field will be interpreted by the browser each time it is loaded.
XSS DOM-Based
This flaw relies on DOM modification via client-side JavaScript, using potentially untrusted data.
It is generally more complex to detect, as the malicious code is not inserted directly into the HTML by the server, but, after the page has been sent and loaded, by the JavaScript executed in the browser.
Vulnerable code:
<script>
const q = new URLSearchParams(window.location.search).get("q");
if (q) {
document.getElementById("results").innerHTML = "<p>Search results for: " + q + "</p>";
}
</script>
URL example:
http://example.com/search?q=<img src=1 onerror=alert(1)>
Why it's vulnerable: The innerHTML
method directly inserts interpretable HTML. Furthermore, in this example, we haven't used a script
HTML tag, as its content may not be interpreted by the browser. Instead, we used an img
tag with an onerror
event to execute the JavaScript code.
In fact, many events can be used in HTML tags, such as onload
, onclick
or onmouseover
, to execute JavaScript code in response to an event.
Here, we use onerror
, which is triggered when an error occurs while loading an image. By injecting an invalid image, the JavaScript code in the onerror
attribute will be executed.
For more information, see this page from Portswigger (BurpSuite's publisher): XSS Cheat Sheet.
Fixing and preventing XSS
Generally speaking, to prevent XSS vulnerabilities, it is essential to encode in HTML all data that can be manipulated by the user before displaying it on a web page. This includes data from forms, cookies, HTTP headers and so on.
For example, in PHP, you can use the htmlentities()
function to encode special characters:
<?php
if (isset($_GET['q'])) {
echo '<p>Search results for ' . htmlentities($_GET['q'], ENT_QUOTES, 'UTF-8') . '</p>';
}
?>
Here we encode special characters in HTML, which prevents the execution of injected JavaScript code. We also use the ENT_QUOTES
parameter to encode single and double quotation marks, and UTF-8
to ensure correct encoding.
To the browser, the code will look like this, so it will interpret the special characters as text:
Example output:
<p>Search results for <script>alert(1)</script></p>
Display in browser (as plain text):
Search results for <script>alert(1)</script>
For DOM-Based XSS, it's recommended to use secure methods to manipulate the DOM, such as textContent
or setAttribute
, which don't interpret HTML. Our example of vulnerable code above can be corrected as follows:
const q = new URLSearchParams(window.location.search).get("q");
if (q) {
const results = document.getElementById("results");
results.textContent = "Search results for: " + q;
}
If you still need to retain certain HTML tags, you can use libraries such as DOMPurify, which allow you to clean up HTML while retaining certain tags.
You can also use sandboxed iframes to display HTML content while blocking JavaScript execution:
<iframe src="data:text/html;base64,<?= base64_encode($content) ?>" sandbox></iframe>
To find out more, take a look at our article on the subject.
Whenever possible, you can also use secure template engines (such as Twig, Handlebars, etc.) which handle escaping automatically and avoid many human errors.
Content Security Policy (CSP)
A Content Security Policy can also be used to reinforce security in case, despite your vigilance, an XSS is still present on your site. This HTTP header limits the resources authorized to run on a web page. This reduces the impact of any XSS:
Example of a CSP header :
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
Learn more about CSP: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
Entry points to watch out for
The data that can be used in an XSS does not only come from GET or POST fields. It can also come from :
- Cookies (used on pages without decoding)
- HTTP headers (User-Agent, Referer, etc.)
- Dynamically constructed HTML attributes
- CSS style sheets injected via WYSIWYG editors
- Variables inserted in dynamically generated JavaScript
It's essential to check and sanitize all user input, regardless of its source.
To go further
In conclusion
XSS are critical, frequent and dangerous vulnerabilities. Preventing them requires rigorous code hygiene, systematic data encoding and the use of secure functions on both server and client sides. Implementing a content security policy (CSP), using secure templates and respecting the principles of separation of responsibilities in code are essential practices.
Secureaks offers a full range of cybersecurity services, including web pentests and code audits. Contact us to secure your applications.