Web Application Security for DevOps: Cross-Origin Resource Sharing (CORS) and Subresource Integrity (SRI)
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:
(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:
- 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. - 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 a401 Unauthorized
or403 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
orDELETE
Access-Control-Request-Headers
: The list of headers that will be sent, including custom request headers.
A preflight request might look something like:
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 asAccess-Control-Max-Age
. - Simple requests don't require preflighting. The rules are:
- The request must only be for the HTTP methods
GET
,PUT
, andHEAD
. - The only headers allowed are
Accept
,Accept-Language
,Content-Language
, andContent-Type
. - If the HTTP method is
POST
, theContent-Type
header must have a value ofapplication/x-www-form-urlencoded
,multipart/form-data
, ortext/plain
.
- The request must only be for the HTTP methods
- 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
: Eitheranonymous
oruse-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!