Minimizing CORS Misconfigurations

January 31, 2023

Cross-Origin Resource Sharing (CORS) is a security mechanism that allows web browsers to only make requests to a different domain if that domain has explicitly granted permission. This is done to prevent malicious websites from making unauthorized requests to other domains and misconfigured CORS can be easily exploited by hackers.

The problem lies in how it is increasingly impossible to rely on single origin, which is easy to trust. Many DevOps teams like to segregate their subdomains (e.g.: first.domain.com and second.domain.com) instead of creating subpaths (e.g.: domain.com/first and domain.com/second), introducing the CORS issue. This wouldn’t be a problem if they pass headers correctly and securely allow credential transmission.

Unfortunately, it is very easy to misconfigure CORS headers. This blog post covers the three solutions we recommend to solving CORS.

Quick Summary: How CORS Works to Prevent Unauthorized Requests

Feel free to skip this section if you already know how CORS works. For an in-depth read on CORS errors, refer to this Mozilla documentation.

Source
Sample image when you encounter a CORS issue.

When making a cross-origin request, the browser adds an Origin header to the request. The server can then check this header to see if the request is allowed. If the server wants to allow the request, it can add the Access-Control-Allow-Origin header to the response with the same value as the Origin header.

However, there are cases where this simple mechanism is not enough. Two examples are:

  • You want to make a request to a subpath of a domain instead of the entire domain

  • You want to pass custom headers with the request

In these cases, you need to use the Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Allow-Credentials headers to specify which methods, headers, and credentials are allowed.

Ultimately, CORS protects end users from malicious sites using an unmodified browser to make a cross-site request to a legitimate site (such as your bank account). If the user already has an authentication cookie, the cookies will be sent along with the request. This can result in man-in-the-middle attacks that bypass 2FA requirements such as Yubikeys because the malicious site can perform seemingly legitimate acts for the user despite not having direct access to the authentication cookie.

Solution 1: Use a Single Domain

Since the CORS issue exists to address issues caused by using multiple domains, DevOps teams can opt to use a single domain instead. This solution opts to use separate routes based on the path (subpaths) instead of the domain name.

For example the routes can be setup as:

Yaml
routes:
  - from: https://app.localhost.pomerium.io
    prefix: /api
    to: http://api:8000
    allow_any_authenticated_user: true
  - from: https://app.localhost.pomerium.io
    to: http://app:8000
    allow_any_authenticated_user: true

In this way all requests to /api will be sent to the API server, and all other requests will be handled by the web application. Update the javascript to use the new domain:

JavaScript
(async () => {
const result =await fetch(location.origin + '/api', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
    },
  });
const json =await result.json();
  console.log('RESULT', json);
})();

And the request will succeed.

Solution 2: Pass the Pomerium Credentials via a Header

Sometimes the DevOps team wants to have subdomains instead of subpaths, so the first solution cannot be used. Since the browser won't send a cookie to a different domain, you can pass the Pomerium authorization JWT via a header instead.

First allow JavaScript to see the cookie with:

Yaml
cookie_http_only: false

And update the JavaScript

JavaScript
(async () => {
  const result = await fetch('https://api.localhost.pomerium.io', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'X-Pomerium-Authorization': 'JWT_TEXT_FROM_COOKIE'
},
});
  const json = await result.json();
  console.log('RESULT', json);
})();

If both domains fall under a shared parent domain (e.g., app.example.com and api.example.com are both under example.com), you can change the Pomerium's cookie domain and share the cookie. Update the Pomerium configuration:

Yaml
cookie_domain: '.localhost.pomerium.io' # note the starting .

And now the cookie will be used for both domains. However, the default browser policy for XHR and Fetch requests is to not pass the cookie, so you also need to change the JavaScript:

JavaScript
(async () => {
	const result = await fetch('https://api.localhost.pomerium.io', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
    },
    credentials: 'include',
  });
	const json =await result.json();
  console.log('RESULT', json);
})();

If you have XMLHttpRequest issues, see withCredentials.

Parting Words

Unless there are extraordinary circumstances necessitating multiple domains or subdomains, we genuinely recommend avoiding the CORS issue altogether by using one domain and subpaths. However, we do understand that sometimes developers inherit pre-existing environments or are told to proceed a certain way, so we hope these solutions help you navigate the CORS headache.

Share:

Stay Connected

Stay up to date with Pomerium news and announcements.

More Blog Posts

See All Blog Posts
Blog
Reference Architecture: Using AWS EKS with Pomerium
Blog
Identity Aware Proxy (IAP): Meaning, Pricing, Solutions
Blog
The Great VPN Myth: What PCI DSS 4.0 Actually Requires for Remote Access

Revolutionize
Your Security

Embrace Seamless Resource Access, Robust Zero Trust Integration, and Streamlined Compliance with Our App.

Pomerium logo
© 2024 Pomerium. All rights reserved