Three C-Words of Web App Security: Part 1 – CORS

For those less versed in web applications and how they’ve evolved, I wrote a sort of prologue to this post back in April 2018, titled A Brief Evolution of Web Apps.

This is the first in a three-part series, Three C-Words of Web Application Security. This one will deal with CORS, while the next will discuss CSRF, including its similarities to attacking a broken CORS policy and what makes it different. The third, and final post in the series will address Clickjacking, and the role it can play in CSRF exploitation.

CORS (cross-origin resource sharing) is actually a policy that loosens a security control called the Same-Origin Policy, or SOP for short. Therefore, in order to understand CORS, we must first understand the SOP.

Certain types of requests are considered lower-risk and are therefore allowed to be cross-origin by default. A few examples are the HTTP Get request generated when an <img> or <script> tag is handled by the HTML rendering engine. Another example is a standard HTML <form> submission. These are generally considered safe because the content is not sensitive or is not directly exposed to the page that called them. In the classic cross-origin form POST scenario, as soon as the form is submitted, the browser essentially navigates to the domain that was submitted to anyway. The response is never passed back to the page that initiated the call, unless the server processing the request explicitly redirects the browser back there.

Other types of request are more high-risk, and are subject to the same origin policy. Typically, these are the XMLHttpRequest (XHR) or more recently the fetch JavaScript APIs. These allow the script executing on the current page to issue HTTP requests and to receive the responses to those requests. One of these APIs, usually XHR, also underpin any library-specific implementations of issuing HTTP requests via browser-based JavaScript, such as jQuery’s $.ajax function. Because these fall under Same-Origin Policy, the browser enforces some security around them.

For simple requests, the browser issues the request, but blocks the response from being handed to the JavaScript through low-level code, if the request was sent to a server at a different origin than the current page. The origin is defined as the unique combination of protocol (http or https), hostname or IP address, and port number. XHR and Fetch requests include an origin header showing the origin of the page issuing the request. For more complex requests, such as those with less common HTTP methods or those with custom headers, a CORS preflight request would be sent first. But let’s set preflights aside for the moment and elaborate on the more simple case.

What if we want, for example, ourhttps://myapplication.secureideas.com modern web app to interact withhttps://api.professionallyevil.com? The Same-Origin Policy is in our way. It’s preventing our application from being able to receive responses from the API. And that’s where the CORS policy comes in. We can loosen our enforcement of same-origin policy by including response headers such as:

access-control-allow-origin: https://myapplication.secureideas.com

This tells the browser that it can make an exception to the Same-Origin Policy if it’s issuing a request from https://myapplication.secureideas.com. In other words, it’s okay to pass the response to the JavaScript. This is a CORS policy, with access-control-allow-origin being the most common CORS header. Now, at this stage, there’s potentially one problem if our API relies on Authentication request headers or on cookies to establish the requestor’s identity. They will not be included in the XHR or fetch request by default. And if we specify the withCredentials flag that instructs the browser to include auth headers and cookies, the Same-Origin Policy kicks in again and refuses to pass the response to the JavaScript. Because allowing the origin is not enough to include these credentials, the response needs to contain another CORS policy header:

access-control-allow-credentials: true

This means it’s okay for the request to be issued with credentials (HTTP authentication request headers and cookies), from the allowed origin. After adding that header to the response, the JavaScript can now issue requests from myapplication.secureideas.com to api.professionallyevil.com, including HTTP Authentication request headers and cookies (which the browser attaches without exposing to myapplication.secureideas.com), and it can receive and handle the responses. Or, to summarize, myapplication can now make authenticated API calls to api.professionallyevil.com as long the user has authenticated.

Where do the security misconfigurations come in? We often want our API to be available to multiple origins, and usually we don’t want to just openly disclose what all those origins are. Therefore, the most common way to implement a CORS policy is to check the origin request header against a whitelist or pattern-match (e.g. regex) first, then include the access-control-allow-origin only if the origin header met the whitelisting criteria. If we don’t include access-control-allow-origin in the response, it’s not an allowed origin, and the Same-Origin Policy will keep the JavaScript in the requesting application from being able to access the response. On several tests for different clients, we’ve found them simply reflecting any origin header into the access-control-allow-origin policy, in combination with allowing credentials. This is the worst possible CORS misconfiguration, as it effectively disables the Same-Origin Policy. Keeping with our previous example, the intent was to allow myapplication.secureideas.com to use api.professionallyevil.com. By reflecting any origin, we allow any site to issue requests to api.professionallyevil.com. For example, a malicious person could set up a rogue site at www.evilpayload.com and using a phishing campaign to direct Secure Ideas users there. If those users are currently authenticated with api.professionallyevil.com, then www.evilpayload.com can make API calls to api.professionallyevil.com as those users. Meaning whatever sensitive data that API exposes to authenticated users can be harvested by any site those users visit (provided they have a valid cookie-based or HTTP auth-based session).

It’s worth noting that the wildcard access-control-allow-origin:* is sometimes used to allow general public access. This is much less concerning than a reflected origin, because access-control-allow-credentials: true will not be enforced with the wildcard origin. In other words, credentials are never allowed with a wildcard origin.

Now, let’s dial that back to a less severe configuration issue. Imagine that Secure Ideas had a multi-tenant app that used the URL-scheme: {tenantname}.secureideas.app, where Company A might have companya.secureideas.app and TKJ Enterprises would access the app through tkj.secureideas.app and so-on. And this app is backed by an API at api.secureideas.app, which (as a different origin from the client portion) must return a CORS policy to allow the pages served by companya.secureideas.app to retrieve data from api.secureideas.com. Now, as the developer of the application, I tend to try to avoid creating resource bottlenecks and unnecessary requests between the tiers of the app. Therefore, in order to avoid having to do some sort of database lookup on every request to make sure the origin is on my whitelist, I’ve simply done a case-insensitive regex match against the pattern https?:\/\/[a-z]+\.secureideas\.app. To summarize that pattern, it’s http with an optional s, literal :// (escaped slashes), one-to-many letters, then .secureideas.app. Now consider what it would mean if the application development team also has a blog at blog.secureideas.app and it’s running a WordPress instance that has a plugin with a cross-site scripting flaw. That origin would match our regex, therefore that unpatched WordPress instance is effectively part of the attack surface for api.secureideas.app. That cross-site scripting flaw would allow an attacker to generate requests against the API using an authenticated user’s session. It would also allow that attacker issue requests against other resources, such as the attacker’s own server. Therefore, exploitation of this flaw could, with trivial effort, exfiltrate a tenant’s data with a simple social engineering attack – directing an authenticated user to the legitimate blog for an application they use. The simplest solution is to modify the pattern or to check against a blacklist of non-tenant subdomains when validating the origin.

Okay, remember that CORS pre-flight that I mentioned earlier? I said it is performed when there are complex requests. Adding custom headers, or using less common HTTP methods (i.e. not GET, POST, or HEAD) or content-types (i.e. any other than: text/plain, application/x-www-form-urlencoded, or multipart/form-data) are the standard criteria for sending a pre-flight. What actually happens in this case is that transparent to the user, the browser sends an HTTP OPTIONS request, to which the server responds with its CORS policy including the headers discussed above, along with several others such as access-control-allow-methods which specifies which HTTP methods should be allowed. The browser will then only issue the specified request if it meets the CORS the policy returned in the OPTIONS response. Because the scenarios above are based around attacking from origins allowed due overly-permissive configurations, the pre-flight would not typically impact exploitability of those flaws.

There are some edge-cases worth considering as well. CORS enforcement is done in the browser, so it is common for servers to respond to trusted and untrusted origins the same way, except for the absent CORS policy. In many cases, this is actually okay, as data-harvesting attack scenarios revolve around the JavaScript consuming the response. One exception is anywhere the data or application state is mutated. In these cases, even if the CORS policy prevents the attacker from reading the response, the attack could still blindly update data or exploit injection flaws. As a result, I recommend checking the origin header early in the request-handling process, and returning an error for non-whitelisted origins.

To summarize, whether you’re defending your application or conducting a penetration test, here are a few things to check:

Does the server respond with a CORS policy? If you’re testing in the context of Same-Origin, you should add the origin header to a request, e.g. with Burp Repeater. Start with an origin that is the same as the legitimate front-end app, rather than some random other hostname.

What permissions are exposed by the CORS policy? Are they using cookies and allowing requests withCredentials? Or are they requiring a custom header or body parameter for authentication (both of which negate most attacks)?

What origins are allowed? Is it reflecting any origin? Or is it just the legitimate application host? Or third-party hosting services e.g. CloudFront URLs?

Will other subdomains work? And if so, are there any other assets included in that scope, other than the client application?

Does it look like they’re doing any sort of pattern matching for origin whitelisting? Is it possible to trick it with a partial match? E.g. if a trusted origin is https://secureideas.com, will insecureideas.com work? What about an overlapping top-level domain, like secureideas.community or secureideas.com.mx?

What about other domains owned by the same company?

What about the public IP address for the server? If so, do other IPs work?

What about other port numbers on the host? Could there be an attack vector via an administrative console or non-production environment?