Web Application Security for DevOps: Cross-Origin Resource Sharing (CORS) and Subresource Integrity (SRI)

Web Application Security for DevOps- Cross-Origin Resource Sharing
Chris Poulin
Written by Chris Poulin
Director of Customer Advocacy and Principal Architect

A Quick Recap of SameSite and Same-Origin Policy

With all of that background from parts 1, 2, and 3 of this series out of the way, let's turn to some practical considerations for real-world web applications. The inherent security restrictions for resources, including cookies and JavaScript, assume that each website contains all of its functionality in one neat, isolated package. But websites often contain content and functionality from multiple websites that trust each other.

Before jumping into Cross Origin Resource Sharing (CORS), let's recap two of the main points we've already covered:

  • Web browsers and other HTTP clients follow SameSite policies for when they provide cookies to a website.
  • JavaScript calls are restricted to the Same-Origin Policy (SOP), as are other types of calls that affect web content, such as video rendering and loading of TrueType fonts.

I'll stick with JavaScript since this isn't meant to be a comprehensive tutorial but rather impart the fundamentals of CORS and other security measures that Bitsight tests for.

Cross-Origin Resource Sharing (CORS)

Note that browsers will refuse cross-origin JavaScript requests for the more dangerous actions, such as trying to read files in local storage or modifying content set by another resource.

As stated in the installment 1 of this series, there's no inherent state mechanism in HTTP, so any given site has no idea which site originated the request. (It knows the browser is making the request, it just doesn't know where the content in the browser came from.) The browser does, though.

Browsers (for brevity, whenever I mention the browser just assume I also mean other compliant HTTP clients) enforce the SOP for JavaScript by letting the site that's a target of the cross-origin request (which I'll call the other site from now on) know which site originated the request. In effect, the browser is tattling on the original site to the other site and letting the other site decide whether to fulfill the request. This is called Cross-Origin Resource Sharing (CORS) and the CORS policy is enforced by the browser.

The mechanism for informing the other site of the origin is the HTTP header with the obvious name Origin.

Let's say https://service.bitsighttech.com contains JavaScript code that makes a call to https://api.bitsighttech.com to display some dynamic content. Remember, even though the two use the same scheme/protocol (https), port (443, the default for https), and share the same second-level domain (bitsighttech.com), they're not from the same origin (although they do match the SameSite criteria).

Here's how to think about the three participating components:

  • The browser (or other HTTP client) is the governor and arbiter, and is considered a trusted agent. It makes the request to the initial site; loads the content, following links to other sites such as loading images; and executes scripts. It's also responsible for implementing the SameSite and SOP rules for sending cookies and JavaScript requests.
  • The origin site contains the main site content, including links to other sites and JavaScript. It may also set cookies. We don't know if it's safe or malicious so it's considered untrusted by default.
  • The other site in this example hosts the API that's called from JavaScript sent from the origin site. This is the site at risk.

Here's what it looks like in practice:

Client browser visualization

(Note that for brevity and simplicity I've omitted the function where the JavaScript above receives the results of the request.)

What's happening in the diagram is:

  1. The client sends the Origin header to inform the other site of the originating site making the request. To be clear, this is the site that's in the URL bar when using a browser.
  2. The other site responds with the Access-Control-Allow-Origin (ACAO) header, informing the client whether the request is authorized from the specified origin.

A CORS request will result in an ACAO response header containing one of:

  • The origin: Indicates the request is authorized. See the diagram above for an example.
  • A wildcard: Indicates the request is authorized from any origin (e.g., Access-Control-Allow-Origin: *).
  • An ACAO header that doesn't reflect the origin that was requested: This may be the header without a value (e.g., Access-Control-Allow-Origin: ) or missing the ACAO header altogether. The server may also respond with a 401 Unauthorized or 403 Forbidden HTTP status code.

CORS Preflight Requests

So far it sounds like the browser just adds an Origin header and lets the other site determine whether the request is authorized. That's true for what the CORS specification considers "simple" requests, which are those that use the GET, POST, or HEAD HTTP methods, and are further restricted to only certain headers (apart from the Origin header) that aren't considered risky:

  • Content-Type
  • Accept
  • Accept-Language
  • Content-Language

If the request uses the POST method, only certain values are permitted in the Content-Type header:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

If the request uses HTTP methods such as PUT or DELETE—methods that can change data—or it uses headers or content types that aren't considered simple, the browser must first ask the other site whether it's willing to accept the HTTP method and/or headers. This is called a preflight request.

The preflight request uses the OPTIONS HTTP method to determine if the server will permit the request. The headers in preflight requests are:

  • Access-Control-Request-Method: The HTTP method, e.g., PUT or DELETE
  • Access-Control-Request-Headers: The list of headers that will be sent, including custom request headers.

A preflight request might look something like:

Client-browser graphic

The other site will respond with Access-Control-Allow- headers containing the methods and headers that it does accept, and the browser can proceed appropriately. The other site can also respond with no Access-Control-Allow- headers (e.g., simply Content-Type: text/plain) or the server may respond with a 401 Unauthorized or 403 Forbidden HTTP status code.

The browser may store the results of the preflight request for the number of seconds specified in the Access-Control-Max-Age response header, if provided, and forgo the preflight check for similar requests for the specified origin, method, and headers until the cache period expires.

Requests With Credentials

By default, browsers will not send credentials in a CORS request. That means no cookies, no HTTP authentication, nor client-side certificates. However, JavaScript can explicitly request that credentials be sent. In the case of the fetch() command, that means including the option, credentials: "include". For example:


fetch("https://example.com", {
  credentials: "include",
});
  

For preflight requests, the browser will only send authentication requests in the follow-up request if the response includes an Access-Control-Allow-Credentials: true header. For simple requests, cookies may be sent to the other site; however, if the site doesn't respond with a Access-Control-Allow-Credentials: true header, the browser will not pass the response to the JavaScript.

Effects of the Wildcard CORS Response

I mentioned earlier that the other site may respond to either a simple or preflight request with a wildcard, Access-Control-Allow-Origin: *. This has the effect that the browser stops enforcing the SOP for scripts requesting access to the other site. It will allow any HTTP method or header to that site.

The good news is there's a positive security side-effect of the wildcard: browsers will no longer send credentials to the other site, including cookies. If the other site wants to allow credentials, it should respond by reflecting the origin in the request back in the ACAO header, regardless of what that origin is. If the browser sends Origin: service.bitsighttech.com;, the other site should reply with Access-Control-Allow-Origin: service.bitsighttech.com;. If the browser sends Origin: someothersite.example;, the other site should reply with Access-Control-Allow-Origin: someothersite.example;. However, in order to signal to the browser that the response is dependent on the request, the other site should send the Vary: Origin header. This tells the browser the response will vary depending on the Origin header in the request and helps it make decisions about caching responses.

CORS Summary

The CORS policy rules and function may seem confusing—and it is complex—so here's an analogy:

Imagine you're a parent with your kids in school (statistically that's likely). The school staff is responsible for the safety of your children and who's allowed to pick them up from school. They're the browser. You're the other site, responsible for authorizing who can pick up the kids from school. Someone shows up at the school claiming to be an uncle and wants to give your kids a ride home; they're the origin of the request. The school has to contact you and get authorization. They'll text you a picture of the individual and you can authorize the pick up or not.

Alright, it's not a perfect analogy, but hopefully it helps crystalize the concept behind CORS.

Finally, here's a summary of CORS:

  • CORS applies to requests from an originating site to another site, when the request is made from JavaScript, web fonts, and a few other resource requests. Images, CSS stylesheets, and media files are generally allowed without SOP restrictions.
  • HTTP clients, including browsers, signal to the other site who's making the request by sending the Origin header.
  • Sites to which the request is made (other sites) respond with Access-Control-Allow- headers, encompassing origins, methods, headers, and credentials, as well as Access-Control-Max-Age.
  • Simple requests don't require preflighting. The rules are:
    • The request must only be for the HTTP methods GET, PUT, and HEAD.
    • The only headers allowed are Accept, Accept-Language, Content-Language, and Content-Type.
    • If the HTTP method is POST, the Content-Type header must have a value of application/x-www-form-urlencoded, multipart/form-data, or text/plain.
  • Preflighting uses the OPTIONS HTTP method to determine whether the origin, methods, and headers are allowed by the other site.
  • Credentials, including cookies, may be sent if the other site responds with Access-Control-Allow-Credentials: true.
  • If the other site responds with Access-Control-Allow-Origin: *, the browser stops enforcing CORS rules for the other site, but will no longer pass credentials to it, including cookies.

Here are some good resources that go into more detail about CORS:

Subresource Integrity Checks

I realize this is already a long installment; however, Subresource Integrity (SRI) is related to CORS, and I hate to separate the two, so please bear with me.

JavaScript libraries and frameworks are often imported from an external source so a site can call predefined and standard functions. For example, Google APIs are widely used for email and forms on a website, and jQuery is a library for simplifying the process of creating scripts. But what if a threat actor compromises the site containing the libraries or injects a vulnerability over-the-wire?

The goal of SRI is to compare a known hash value for the imported resource to ensure it hasn't been tampered with. The resource, a JavaScript library in this case, from the external source is called a subresource. A hash value for the subresource is precomputed and included in the HTML tag on the site importing the subresource. When the site retrieves the subresource, it computes the hash value and compares it with that in the HTML tag to ensure its integrity.


<script src="https://javalib.example/framework.js" 
        integrity="sha384-M6kredJcxdsqkczBUjMLvqyHb1KJThDXWsBVxMEeZHEaMKEOEct339VItX1zBl6y" 
        crossorigin="anonymous"></script>
    

A hash value is a string that uniquely identifies some text, a file, or any other data resource. If one byte of the source is changed, the computed hash value changes.

Hashing involves feeding the resource through an algorithm that produces a short block of text that's always the same length for a particular algorithm regardless of the length of the input, whether it's the entire text of "War And Peace" or this sentence. Hash values are often used as digital signatures.

Where:

  • src: The URL of the JavaScript library.
  • integrity: Contains the hash algorithm, sha384 in this case, followed by a hyphen and the hash value. Note the value may contain multiple hash algorithm/value pairs separated by whitespace.
  • crossorigin: Either anonymous or use-credentials. use-credentials: Tells the browser to include credentials if the request for the library is cross-origin (see Requests With Credentials in the CORS section). anonymous: Does not include credentials.

When the web developer creates a site that includes an external JavaScript library (subresource), they compute the known-good hash value and include it in the HTML tag that references the library. The browser computes the hash value on-the-fly when the page is rendered, and will refuse to run the JavaScript if the hash value doesn't match that in the HTML tag.

SRI is optional; however, a site can set the header Content-Security-Policy: require-sri-for script to make it mandatory.

We'll cover Content-Security-Policy and other miscellaneous web application security topics in the last installment of this series. For now, relax and congratulate yourself on getting through the complexities of CORS. If it was half as difficult to understand as it was to explain, we both deserve a week of not thinking about it!