Networking & Content Delivery
CORS configuration through Amazon CloudFront
Cross-origin resource sharing (CORS) is a security feature implemented by web browsers that controls which web pages or web applications are allowed to make requests to a different domain or origin. In other words, CORS is a mechanism that prevents a web page hosted on one domain from making requests for resources from a different domain unless the target domain explicitly allows it. This helps protect against potential security vulnerabilities such as cross-site request forgery (CSRF) and makes sure that sensitive data isn’t accessed by unauthorized websites. CORS is typically configured on the server-side by adding specific HTTP headers to responses, indicating which domains are permitted to access its resources. Furthermore, it applies to resources fetched through XMLHttpRequests, Fetch API, Web Fonts, and WebGL textures that are requested from a different domain from the one serving the web page. This helps make sure of secure and controlled data sharing between different websites or web applications.
Cross-origin working and the significance of CORS request/response headers
Initially the browser/client sends an HTTP request to the application server, receives data as an HTTP response, and displays it. In browser terminology, the current browser URL is called the current origin and the third-party URL is the cross-origin.
When you make a cross-origin request, this is the request-response process:
- Initiation: Browser sends an HTTP request from a web page hosted on one domain (domain, scheme, or port) to a different origin.
- Preflight request (optional): If the request is a non-simple request or uses certain methods (such as PUT, DELETE) or has headers (such as Authorization), then the browser may initially send a preflight OPTIONS request to the server to determine if the actual request is safe to send.
- Server processing: The server evaluates its CORS policy to determine whether it allows requests from the origin of the requesting domain. It does this by inspecting the origin request header.
- CORS response headers: If the server allows requests from the origin, then it includes specific CORS headers in the response, such as Access-Control-Allow-Origin along with other headers, indicating which origins are allowed.
- Browser validation: The browser checks the CORS headers in the response. If the response headers allow the request, then the browser proceeds with sending the actual request. Otherwise, it blocks the request and may throw a CORS related error in the developer console.
- Response handling: The browser sees the access control request headers and shares the returned data to the client application.
Figure 1: CORS preflight request workflow
Alternatively, if the server doesn’t want to allow cross-origin access, then the browser responds with an error message. Depending on the nature of the requests, the cross-origin request flow can differ, as shown in the preceding figure. In the following sections, we discuss Simple CORS and Preflight CORS requests.
Simple CORS requests
Simple CORS requests are the most direct type of CORS request. They are automatically generated by modern web browsers when certain conditions are met, allowing for cross-origin requests without the need for a preflight request.
To qualify as a Simple CORS request, the following conditions must be met:
- HTTP methods: Only HTTP methods such as GET, POST, or HEAD are allowed. If the request uses any other method, then it triggers a preflight request.
- Content-Type header: The Content-Type header in the request must be one of the following: application/x-www-form-urlencoded, multipart/form-data, or text/plain. If the request uses any other content type, then it triggers a preflight request.
- Custom headers: Custom headers can be included in the request, but only specific headers defined as simple headers by the CORS specification are allowed. These simple headers include common ones such as Accept, Accept-Language, Content-Language, and Range (only with a simple range header value). Any non-simple header triggers a preflight request.
Simple CORS requests are less complex and faster to execute because they don’t involve a preflight check with the server. Instead, the browser checks if the preceding conditions mentioned are met and, if so, allows the request to proceed.
> GET /test.html HTTP/2
> Host: d1234test.cloudfront.net
> User-Agent: curl/8.1.2
> Accept: */*
> Origin: www.example.com <----- indicates the origin that caused the request
In this case, the server must return the Access-Control-Allow-Origin response header.
Preflight CORS requests
Preflight CORS requests are more elaborate than Simple CORS requests and are used when a request doesn’t meet the criteria for a Simple CORS request. A preflight request is an HTTP OPTIONS request that is sent to the server before the actual request to make sure that the server supports cross-origin requests from the requesting domain.
Key factors that trigger preflight requests include:
- Non-simple headers: When the request includes custom headers that aren’t considered “simple” headers, a preflight request is generated.
- Credentials: If the request includes credentials (for example cookies, HTTP authentication), then a preflight request is typically needed.
The preflight request includes the HTTP method through the Access-Control-Request-Method
request header and other headers that are used in the actual request through the Access-Control-Request-Headers
header. The server must respond with appropriate CORS headers indicating whether the requested cross-origin interaction is permitted. If the server approves, then the browser proceeds with the actual request.
> OPTIONS /test.html HTTP/2
> Host: d1234test.cloudfront.net
> User-Agent: curl/8.1.2
> Accept: */*
> Origin: www.example.com <----- indicates the origin that caused the request
> Access-Control-Request-Method: GET
> Access-Control-Request-Headers: x-custom
> GET /test.html HTTP/2 <----- Actual request
> Host: d1234test.cloudfront.net
> User-Agent: curl/8.1.2
> Accept: */*
> Origin: www.example.com <----- indicates the origin that caused the request
> x-custom: custom-value
In this case, the server must respond with these response headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers for preflight request.
Configuring CORS from the Amazon CloudFront end
First option: Response header policies
Amazon CloudFront allows you to control/customize response headers being sent back to the viewers through response header policies with little or no conditionality, as shown in the following figure. Response headers policies streamline the process of HTTP header response manipulation so that you can define CORS, security, and custom response headers as a configuration setting in CloudFront through the console or the API. By using response headers policies (RHP), you don’t need to write the code. Instead, you can use the console or API to define CORS/Security/custom headers. RHP comes free of cost and is easy to configure.
Figure 2: Handling CORS with CloudFront
If you have a Simple CORS use-case requirement, then you can use the Simple CORS AWS Managed RHP.
If your use-case has a preflight request (also known as an OPTIONS request) as well, then you can use CORS-With-Preflight RHP.
These policies cover a wider audience, and you want specific policies where you can allow only specific origin, headers, or methods. Therefore, you can create your own RHP for a custom CORS requirement. To create RHP, you can refer to this CloudFront developer guide.
Second option: CloudFront functions/Lambda@Edge
If you have a requirement of handling OPTIONS directly at the CloudFront level or having a requirement of a preflight worker, then you can configure CloudFront Function on the viewer-request event. The following code is the sample code that would respond back with CORS headers for the OPTIONS method request directly from the edge without the need to forward the requests to the origin server. This decreases latency and also reduces the load on origin, thus improving the overall performance of your web application.
Sample code:
function handler(event) {
var request = event.request;
var response = {
statusCode: 200,
statusDescription: 'Ok',
headers:
{ "access-control-allow-methods": { "value": "GET" } }
};
var headers = response.headers;
// If origin header is missing, set it equal to the host header.
if (request.method == 'OPTIONS'){
headers['access-control-allow-origin'] = {value: "https://example.com"};
headers['access-control-allow-methods']= {value: "GET"};
headers['access-control-allow-headers']= {value: "X-PINGOTHER"};
return response;
}
return request;
}
For actual requests, you can use the CloudFront response header policy that allows you to configure and customize CORS headers directly at the edge.
However, there are various use-cases where the requirement is to provide CORS header values in the response based on certain conditions. For example, based on ‘access-control-allow-origin’ header values, you need to define the ‘access-control-allow-headers’ value. Then, the edge functions on the viewer/origin response events would be needed because RHP can’t perform conditional checks. The sample code can be found in this CloudFront developer guide.
If the origin encounters errors such as server-side failures or the origin is unreachable, then CORS headers may not be correctly set or could be missing, which can lead to CORS errors. Proper CORS handling in these situations is vital for keeping the web applications secure and reliable. Therefore, in these cases, you can use Lambda@Edge at the origin-response event where you can write custom code and dynamically adjust CORS headers as needed, even when the origin encounters errors.
Third option: CORS handling at origin
Although CloudFront offers robust CORS handling mechanisms, the advantages of handling CORS at the origin servers are substantial. The ability to accomplish granular control, consistent CORS handling across multiple CDNs, and enhanced security through origin specific policies can significantly impact the security and performance of web applications.
When you have enabled CORS on an Amazon S3 bucket or a custom origin, you must choose specific headers to forward through Cache policy or Origin Request policy to respect the CORS settings. The headers that you must forward differ depending on the origin (Amazon S3 or custom origin) and whether you want to cache the OPTIONS responses.
However, you can experience inconsistent CORS issues if you configure the Managed CORS (Amazon S3 or custom origin) origin request policy to forward these headers and when you have enabled caching through the cache policy (for example, Managed CachingOptimized policy). In this case, if CloudFront receives a first request that doesn’t contain an ‘Origin’ header, then by default it caches the first response without the CORS response and uses this for all future requests for this object until it expires. If a user requests the same object before it expires, then they don’t get the proper CORS response headers and the browser would fail to load the asset, even if the subsequent request contains the ‘Origin’ header.
The ‘Origin’ header is added in the viewer request to CloudFront, which CloudFront forwards to the origin. Optionally, you can configure a CloudFront Function that adds an ‘Origin’ HTTP header to the request if the request doesn’t already contain this header. This makes sure the ‘Origin’ header is forwarded in each request to CloudFront and then further to the origin.
Conclusion
In conclusion, handling CORS in Amazon CloudFront is critical to making sure of smooth and secure interactions between web applications and resources hosted on different origin servers. The CloudFront response header policy allows a centralized and efficient way of managing CORS configurations by enabling control over allowed origins, headers, and methods.
Moreover, CloudFront Function and Lambda@Edge Function offer the necessary flexibility to implement custom CORS logic directly in the edge, and they allow for seamless integration with existing workflows. Whether it’s modifying response headers, dynamically generating CORS headers based on certain conditions, or implementing complex CORS rules, these edge functions offer a scalable and efficient option. Alongside the added benefits, there are further costs incurred when using edge functions as compared to configuring the CloudFront response header policy, which comes at no extra cost. You should assess the cost-effectiveness based on specific use-cases.
However, for scenarios where extensive CORS handling requirements or fine-grained control is necessary, handling CORS directly at the origin server is a viable option. This allows you to retain full control over the CORS policy, and this could be very useful in multi-CDN architectures where you can have the CORS policy set at a single place for different CDNs.
For CORS handling, CloudFront offers a spectrum of options such as response header policy, edge functions, and the flexibility to handle CORS at the origin server. However, the choice of approach depends on certain factors such as complexity, scalability, and the requirements of the application’s architecture.
About the authors