Want to learn more about OAuth 2.0 and OpenID Connect?
Save yourself days of digging through dozens of specs with this online course
More informationPreventing XSS in React (Part 1): Data binding and URLs
A Cross-Site Scripting (XSS) vulnerability can and will lead to the full compromise of a frontend application. An XSS vulnerability allows the attacker to control the application in the user's browser, extract sensitive information, and make requests on behalf of the application. Modern frameworks come with built-in defenses against XSS, but how far do they really go? In this article series, we look at how React prevents XSS, but also how its shortcomings leave a lot in the hands of a developer. This article is the first in a series of three.
13 May 2020 SPA Security XSS, React, Single Page Applications
A short primer on XSS
An XSS vulnerability is an injection vulnerability, where the attacker inserts a piece of malicious data into a web application. The maliciously injected data will be picked up by the browser, which interprets the data as code. The result is that the injected payload from the attacker will be executed as legitimate application code, giving the attacker full control over the application running in the user’s browser.
The code example below illustrates a textbook example of an XSS vulnerability and attack.
A DOM-XSS vulnerability in a jQuery application
<div id="messageContainer"></div>
<script>
...
$("#messageContainer").append(myMessage);
...
</script>
A malicious message containing a JavaScript payload
Hello John!<script>alert("Let's play hide and seek!")</script>
If this data is inserted into the page, the script in the message will be executed, triggering the alert dialog. The example here calls the alert()
function, which is probably the least dangerous effect of an XSS attack. Make no mistake. In reality, attackers can steal sensitive data from the pages, capture user input to steal passwords or credit card information, and even send requests to servers as if the legitimate application itself sends them. Once an attacker succeeds in exploiting an XSS vulnerability, you should consider the entire execution environment of the application in the browser to be compromised.
The best defense against XSS attacks is to ensure that the browser will never see data as code. A common approach to achieve that is by applying context-sensitive output encoding. Context-sensitive output encoding will ensure that the data is encoded for the context where it ends up in the page. During the encoding, potentially dangerous characters are translated into harmless counterparts. Because of that translation, the browser will see these characters as data instead of code, which avoids the confusion and prevents the attack.
Below is an example of the effect of applying context-sensitive output encoding on data containing a malicious payload.
A malicious message containing a JavaScript payload, but properly encoded to avoid XSS
Hello John!<script>alert("Let's play hide and seek!")</script>
As you can see, the browser is now instructed to render the <
and >
HTML codes, which display the <
and >
characters. Since this is clearly data, the browser will not confuse the data for code.
React applies auto-escaping
When an application creates new elements through the React APIs, React is aware of the potential danger of XSS. As a result, React will automatically ensure that data that ends up in the page will not cause XSS attacks. The code snippet below shows a code example of the createElement()
API.
React auto-escapes the children of an element (the third argument to createElement)
return React.createElement("p", {}, review);
Feeding this API data containing HTML elements will ensure that all dangerous characters are encoded before the data ends up in the page. This defense mechanism is crucial to ensure baseline protection against XSS attacks. When server-side frameworks and client-side frameworks fail to offer this minimal level of protection, applications using those frameworks often suffer from a vast amount of XSS vulnerabilities.
React takes it even a step further. Properties added through the props
object are properly protected. If an attacker tries to inject content into dynamic attributes, such as a style attribute, React will detect that behavior and refuse that input. The code example below illustrates how to provide an individual property through the createElement()
API.
React ensures that only legitimate values are used in the props object
let color = 'red; background-color: blue';
return React.createElement("p", {style: { color: color }}, 'paint me blue!');
When this code executes, the style
attribute will not contain a color
directive, since the value was invalid. Note that this protection applies to values assigned to a key in the props
object. If the attacker gains full control over the entire object, React cannot protect against such injections.
JSX has your back!
The React APIs are aware of common XSS vulnerabilities and apply the necessary protection. That same protection is applied when the components are generated from JSX code, instead of through the React APIs. The snippet below shows a concrete example, where we bind the data from a review into the HTML of the page.
JSX auto-escapes the data before putting it into the page
return ( <p>{ review }</p> );
When this component is rendered, the data in the review will be placed inside the HTML p
tag. Thanks to the auto-escaping applied by React, malicious content in the review will not be seen as code. Even if the attacker successfully injects the malicious review from before, the application will render the code instead of executing it. The screenshot below shows how the output from binding the review through JSX.
Just like before, the same protection applies to variables in HTML attributes. The code snippet below shows the JSX equivalent for dynamic styling.
JSX auto-escapes the data before putting it into the page
return ( <p style={{color: reviewColor}}>{rating}</p> );
An attacker controlling the reviewColor
variable could provide additional CSS style code as input, such as red, background-color: yellow
. The JSX parser translates the object into style attributes and detects this invalid CSS input. As a result, all style information will be dropped from the element, and the attack will be neutralized.
Wait a minute, what about URLs?
The secure-by-default handling of simple data binding in React is fantastic. Unfortunately, XSS vulnerabilities can be introduced in other places than the contents or attributes of HTML elements. One notorious example is a URL. Take a look at the URL shown below.
All modern browsers support JavaScript URLs in element attributes
javascript:alert('Don't laugh, this is not a joke!')
This URL uses javascript:
as the scheme, instead of http:
or https:
. When a browser sees such a URL in an HTML document, it typically sees it as JavaScript code that needs to be executed. You can try out what happens if a javascript:
URL ends up in the href
of an a
element by clicking here.
The a
tag is one example where such URLs are considered valid, and an iframe
element is another. More examples are available in this OWASP XSS Filter Evasion Cheat Sheet.
When the URL is hardcoded, there is no XSS vulnerability. However, when the URL is provided by the user, as shown below, there is a potential XSS vulnerability.
If the attacker controls a URL, it can lead to XSS attacks
return ( <a href={url}>More information</a> );
Avoiding URLs as input is the most effective security strategy. For example, an application that accepts Youtube URLs as input could only accept the video ID as input. The rest of the URL can be created when needed by embedding the video ID into a fixed URL. This strategy prevents the attacker from controlling the URL scheme, eliminating the risk of XSS through a URL.
Unfortunately, avoiding URLs as input is not always possible. The restaurant review application, for example, accepts URLs for the restaurant’s website as input. The application uses those URLs in an a
element, to allow the user to visit the web page of the restaurant. But how can you secure such a use case?
Learn how to secure your React applications?
This online course dives deep into CSP and Trusted Types for React
More informationThe darkness of JavaScript URLs
Today, it is safe to say that javascript:
URLs are a painful mistake from the past. Unfortunately, applications have come to rely on this behavior for legitimate features, making it difficult to turn this off at the browser level. However, more and more modern application frameworks are discouraging or preventing the use of javascript:
URLs. React is one of them.
If React detects the use of a javascript:
URL during development, it will show a browser console warning stating that future versions of React will prevent such behavior. The screenshot below illustrates such a warning, displayed as an error. The message states the following:
Warning: A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed "javascript:alert(1)".
Awesome. This warning is a great way to inform developers of the potential dangers of using javascript:
URLs. However, this feature does not prevent the use of such URLs. It merely warns about them. That means that you will have to take concrete steps to ensure that URLs coming from untrusted content are safe. We will look at that next.
Before we move on, note that the warning talks about using dangerouslySetInnerHTML
, a topic we will discuss in the next article in this series.
URL sanitization in React
Before we look at actual defenses, let’s take a look at how React detects unsafe javascript:
URLs. The code is located right here, and the relevant snippet from that file is shown below.
React code to detect potentially dangerous JavaScript URLs
const isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i;
As you can see, the code uses a regular expression to look for a known bad pattern. While effective in this case, looking for known bad values is a security anti-pattern. In this case, it only covers URLs starting with javascript:
. URLs starting with data:
are not covered, even though these can also lead to XSS attacks. The code example below shows how to use a data:
URL to trigger script execution in an iframe. While browsers are starting to prevent such abuses by default, applications still need to protect against such attacks.
An attacker providing a malicious frame URL can trigger XSS through a data URL
<iframe src="data:text/html,<script>alert(1)</script>"></iframe>
Unfortunately, React does not have any built-in defenses against such URLs. There is also little to no security advice available on how to handle such cases. But looking at other frameworks yields interesting results.
What about Vue?
The Vue Security page mentions this case explicitly and refers to the @braintree/url-sanitize
library. This library detects unsafe patterns and replaces them with about:blank
. While this definitely works from a security perspective, a a closer look at the code again reveals a code pattern looking for known bad URLs. The pattern is also overly restrictive and considers all data:
URLs to be off-limits, even though some are benign (e.g., images, audio, video, …).
Borrowing code from Angular
Angular is a different story. Angular comes with a built-in sanitizer for URLs, which is automatically enabled. The Angular sanitizer ensures that dynamically-created URLs are safe to use in the application. A look at the code reveals an entirely different approach. Instead of looking for known bad patterns, Angular approves known safe URLs. Everything that does not match known-good values is blocked by default.
As a security expert, I am a fan of Angular’s approach to URL sanitization. Given that the code for this aspect of the Angular sanitizer is small and completely independent of the rest of the framework, I recommend using this code to sanitize URLs. Below, you can find the snippet of code needed to implement URL sanitization.
URL sanitization code based on Angular's sanitizer
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi;
/** A pattern that matches safe data URLs. It only matches image, video, and audio types. */
const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+\/]+=*$/i;
function _sanitizeUrl(url: string): string {
url = String(url);
if (url === "null" || url.length === 0 || url === "about:blank") return "about:blank";
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url;
return `unsafe:${url}`;
}
export function sanitizeUrl(url = "about:blank"): string {
return _sanitizeUrl(String(url).trim());
}
Secure coding guidelines
That brings us to the summary of this first part on Preventing XSS in React. There are two concrete takeaways.
First, you should analyze your application code for any dynamically generated URLs. These are typically found in href
and src
attributes of HTML elements. Whenever a URL is created with untrusted data, you need to make sure that the URL is safe to use.
Second, to ensure all developers in your team, project or organization use the same secure URL sanitization code, you should encapsulate this feature in a library. I will leave in the middle, whether it should be a small library that supports only URL sanitization or a more extensive library that also supports other security features.
If you want to go for the overall security library, keep an eye out for the upcoming post on safe HTML handling (Subscribe to the mailing list!). We’ll revisit the error message from React, which states that If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead..
Finally, I also built a cheat sheet on preventing XSS in React applications. Make sure you grab a copy and share it with your friends and colleagues.ut using dangerouslySetInnerHTML
, a topic we will discuss in the next article in this series.
All articles in this series
- Preventing XSS in React (Part 1): Data binding and URLs (this article)
- Preventing XSS in React (Part 2): dangerouslySetInnerHTML
- Preventing XSS in React (Part 3): escape hatches and component parsers
Want to learn about defense-in-depth mechanisms for React applications?
Join this course to learn how to secure your React applications
More informationAbout Dr. Philippe De Ryck
Hi, I'm Philippe, and I help developers protect companies through better web security. Learn more about my security training program, advisory services, or check out my recorded conference talks.
Want to learn more about OAuth 2.0 and OpenID Connect?
Save yourself days of digging through dozens of specs with this online course
More informationDr. Philippe De Ryck
Hi, I'm Philippe, and I help developers protect companies through better web security. As the founder of Pragmatic Web Security, I travel the world to teach practitioners the ins and outs of building secure software.
Talks and workshops
You will often find me speaking and teaching at public and private events around the world. My talks always encourage developers to step up and get security right.
Articles
Security is often about small nuances. In my articles, I dive deeper into various security topics, providing concrete guidelines and advice. My articles also answer questions I often get while speaking or teaching.
Security resources
Getting security right is all about knowledge. I strongly believe in sharing that knowledge to move forward as a community. Among my resources, you can find developer cheat sheets, recorded talks, and extensive slide decks.
Mailing list
Subscribe to the Pragmatic Web Security mailing list to stay up to date on the latest activities and resources.