DEV Community

Cover image for Your rich text could be a cross-site scripting vulnerability
IBM Developer for IBM Developer

Posted on • Originally published at developer.ibm.com

Your rich text could be a cross-site scripting vulnerability

Recognize and mitigate SXSS vulnerabilities before they're exploited

By Luke Harrison

This article was originally published on IBM Developer.

Many current applications need to render rich text in HTML on their websites. In order to generate this formatted text from user input, developers use a rich text editor component. The problem? This functionality can indirectly open up both your application and data to a vulnerability known as stored cross-site scripting (SXSS).

In this article, you'll learn what an SXSS vulnerability is and review some "code smells" that you can use to check whether your applications are affected. You'll also see an example of a vulnerable application and learn a remediation strategy for this vulnerability.

What is stored cross-site scripting?

Stored cross-site scripting is a type of vulnerability that attackers can exploit to inject malicious code into a database. That code then runs on the victim's browser after being fetched and rendered by a front-end framework.

This vulnerability is extremely dangerous because it can enable attackers to steal cookies, trigger redirects, or run an assortment of dangerous scripts in the victim's browser. It requires very little work on the attacker's part to propagate the exploit: the victim doesn't need to click on a malicious link or fall for a phishing scheme, they simply use a trusted site affected by SXSS. Check out the links at the bottom of the page for more details regarding cross-site scripting vulnerabilities.

Code smell: innerHTML and dangerouslySetInnerHTML

A code smell is simply a characteristic in code that indicates a deeper problem. Browsers won't normally run injected scripts automatically, but if a devoloper uses some potentially dangerous browser APIs or element properties, it can lead to a situation where the scripts do run.

Have a look at the following code snippet:

const someHTML = <h1>Hello world</h1>
const output = document.getElementById("rich-text-output");

output.innerHTML = someHTML
Enter fullscreen mode Exit fullscreen mode

In this example, we store some HTML in a variable, fetch an element from the DOM, and set that element's innerHTML property to the content stored in the variable. The innerHTML property can be used to render HTML from a string inside another HTML element.

What's dangerous about this property is that it will render any HTML or JavaScript you pass into it. That means if someone was able to control the data that was passed into the property, they could technically run any JavaScript in a user's browser.

Another popular but dangerous way to render dynamic HTML in a browser is by using the dangerouslySetInnerHTML React component property. This property behaves exactly the same way as the innerHTML property in vanilla JavaScript and HTML.

The following example appears in the React docs:

function createMarkup() {

  return {__html: 'First &middot; Second'};

}

function MyComponent() {

  return <div dangerouslySetInnerHTML={createMarkup()} />;

}
Enter fullscreen mode Exit fullscreen mode

If you are currently using either of these properties in a front-end web application, there's a good chance you have some type of cross-site scripting vulnerability. We'll look at how these properties can be exploited and some steps you can take to remediate these issues later in this article.

Code smell: Rich text editors

Another sign that your application might vulnerable to SXSS is simply whether or not you are using a rich text editor, such as TinyMCE or CKEditor.

TinyMCE

CKEditor

Most rich text editors work by converting formatted text generated by a user into HTML. As an added security measure, many of these editors employ some form of sanitization to remove potentially malicious JavaScript from their inputs. However, If you are not applying these same sanitization techniques on the services that receive and store the rich text content, then you are likely making your applications vulnerable to SXSS.

Even if you are not rendering the content on your own sites, there's a good chance that this data could be consumed by applications that do render. To design secure applications, it's extremely important that you consider the current and future consumers of your data. If your data is affected by SXSS then so are all the applications that consume your data.

Example application with SXSS vulnerability

Let's take a look at a small example of a web application with an SXSS vulnerability and then attempt to exploit it.

To run this application, first clone this demo app repo and follow the "Running the application" instructions in the readme.md file.

After running the application and going to http://localhost:3000/unsanitized.html, you should see a page that looks like this:

vulnerable web app

This application simply takes some rich text input from a user, stores it on a web server, and then renders it in the section labeled Output.

Before we exploit the SXSS vulnerability, take a moment to have a look at the application. Refer to the code smells mentioned above and scan through the code to see if you can spot the troublesome sections. Try opening the network tab in your browser and see the requests it sends when you enter and submit some rich text.

In the unsanitzed.html file, you will see the following function, named renderPostByID:

const renderPostByID = async (id) => {
    // setting url seach params
    let newURL = window.location.protocol + "//" + window.location.host + window.location.pathname + `?post=${id}`;
    window.history.pushState({ path: newURL }, "", newURL);

    // getting rich text by post id
    let response = await fetch(`/unsanitized/${id}`, { method: "GET" });
    let responseJSON = await response.json();
    console.log(responseJSON);

    // rendering rich text
    output.innerHTML = responseJSON.richText;
};
Enter fullscreen mode Exit fullscreen mode

Look carefully at this function. You'll notice that we are using the afformentioned innerHTML property to render some rich text that we fetched from the API in HTML form.

Now that we see the vulnerable portion of the code, let's exploit it. We'll bypass the rich text editor input and hit the API endpoint that saves posts to the web server directly. To do this, you can use the following cURL command:

curl --request POST \
--url http://localhost:3000/unsanitzed \
--header 'Content-Type: application/json' \
--data '{
"richText": "<img src='\''x'\'' onerror='\''alert(1)'\''>"
}'
Enter fullscreen mode Exit fullscreen mode

Notice the data payload we are sending in the request. This is some maliciously crafted HTML that includes an image tag with a onerror property set to some JavaScript that displays an alert dialog. Attackers will use tricks like this to avoid poorly implemented sanitization methods that aim to strip JavaScript from HTML elements before they are stored in a database.

After running the script above, you should receive a post ID like the following:

{"id":"1efa54c3-4d13-6c80-b1e8-a942fe26c532"}
Enter fullscreen mode Exit fullscreen mode

Paste this post ID into the post URL query parameter, and press Enter.

post id

When you do this, you should see an alert dialog on your screen confirming the site is indeed vulnerable to SXSS.

alert dialog

How to prevent SXSS

Now that we have seen how to exploit an SXSS vulnerability, let's take a look at how we can remediate one. To do this, you'll need to sanitize the HTML-based rich text in three different places:

  1. Server side, before the content is stored in your database.
  2. Server side, when the content is retrieved from your database.
  3. Client side, when the content is rendered by the browser.

It might be clear why you want to sanitize the content before storing it in the database and when rendering it on the client side, but why sanitize when retrieving it? Well, let's imagine someone obtains the privileges necessary to insert content directly into your database. They could now directly insert some maliciously crafted HTML, completely bypassing the initial sanitizer. If a consumer of one of your APIs is not also implementing this sanitization on the client side, they could fall victim to the cross-site scripting exploit.

Keep in mind, though, adding sanitization to all three locations could cause performance degradation, so you will need to decide for yourself if you require this level of security. At the very least, you should be sanitizing any data on the client side before rendering dynamic HTML content.

Let's take a look at how we implement sanitization in the secure version of our vulnerable application. Since this application is primarily written using JavaScript, we use the dompurify library for the client side and the isomorphic-dompurify library for server-side sanitization. In the app.js program that acts as our web server, you will find an express endpoint /sanitized with a GET and POST implementation:

app.post("/sanitized", (req, res) => {

  let richText = req.body.richText;

  let id = uuid.v6();

  data[id] = DOMPurify.sanitize(richText); // insert sanitized input

  res.json({ id });

});

app.get("/sanitized/:id", (req, res) => {

  let id = req.params.id;

  let richText = DOMPurify.sanitize(data[id]); // retrieve sanitized input

  res.json({ richText });

});
Enter fullscreen mode Exit fullscreen mode

In the POST implementation, we first retrieve the rich text from the body of the request and then call the sanitize method of the isomorphic-dompurify library before storing it in our data object. Similarly, in the GET Implementation, we call the same method on the rich text after retrieving it from our data object and before sending it to our consumer.

On the client side, we again use this same method before setting the innerHTML property of our output div in sanitized.html.

// rendering rich text
output.innerHTML = DOMPurify.sanitize(responseJSON.richText);
Enter fullscreen mode Exit fullscreen mode

Now that you've seen how we properly sanitize HTML to prevent cross-site scripting, go back to the original exploit for this application and run it again, this time using the sanitized endpoint. You should no longer see the alert dialog pop-up, since we are now using the proper techniques to prevent the SXSS vulnerability.

For a full SXSS guide, including best practices and other techniques for preventing XSS, take a look at the OWASP Cross-Site Scripting cheat sheet.

Summary and next steps

In this article, we've looked at how you can increase your application security posture by preventing stored cross-site scripting, a common type of web application vulnerability. You should now be able to recognize whether your own applications are vulnerable, which features you need to review, and how to mitigate before malicious actors can exploit those vulnerabilites.

Security is paramount for enterprise developers. Use the following resources to continue building your awareness of possible vulnerabilities and the ways in which you can improve your security posture.

Top comments (0)