This chapter covers CORS (Cross-Origin Resource Sharing) configuration for Amazon S3 in the context of web applications, a critical topic for the DVA-C02 exam. CORS is essential when your web application (e.g., on domain example.com) needs to access resources stored in an S3 bucket (e.g., on s3.amazonaws.com). The exam tests your ability to configure CORS rules correctly, understand the preflight request mechanism, and troubleshoot common CORS errors. Approximately 5-10% of exam questions touch on CORS, often integrated with S3, API Gateway, or CloudFront scenarios.
Jump to a section
Imagine a VIP club (the web application on domain B) that allows entry only to people on a pre-approved guest list. A person from a different city (a web app on domain A) wants to enter the club to get a special drink (a resource like an image or API response). The club's bouncer (the browser) first checks if the person is from a trusted city. The bouncer sends a preliminary request (a preflight OPTIONS request) to the club's management (the server) asking: 'Is it okay if someone from City A comes in? What actions are they allowed to do? Can they bring their own drink (custom headers)?' The club responds with a list of allowed cities (AllowedOrigins), allowed actions (AllowedMethods), and allowed items (AllowedHeaders). If City A is on the list, the bouncer lets the person in and gives them the drink. If not, the bouncer denies entry and the person never gets the drink. The person never even sees the club's interior if the preflight fails. This mirrors CORS: the browser enforces the same-origin policy, and CORS is a mechanism for servers to relax that policy by explicitly listing which origins, methods, and headers are permitted.
What is CORS and Why Does It Exist?
CORS (Cross-Origin Resource Sharing) is a security mechanism implemented by web browsers to control how resources on one origin can be accessed by a web page from a different origin. An "origin" is defined by the scheme (protocol), host (domain), and port. For example, https://www.example.com:443 and http://api.example.com:80 are different origins even though the host is similar. The same-origin policy (SOP) prevents a web page from making requests to a different origin unless explicitly allowed. CORS provides a way for servers to relax SOP by including specific HTTP headers that tell the browser which origins are permitted.
In the context of S3, when a web application hosted on https://myapp.com tries to load a JavaScript file or an image from an S3 bucket https://mybucket.s3.amazonaws.com, the browser checks if the S3 bucket allows https://myapp.com via CORS headers. Without proper CORS configuration, the browser blocks the request and the developer sees a CORS error in the console.
How CORS Works Internally – The Mechanism
CORS involves two types of requests: simple requests and preflight requests.
Simple Requests – A request is considered simple if it meets all of the following conditions:
Method is GET, HEAD, or POST.
No custom headers are added; only CORS-safelisted request headers (Accept, Accept-Language, Content-Language, Content-Type with values application/x-www-form-urlencoded, multipart/form-data, or text/plain) are used.
No event listeners are registered on the XMLHttpRequest upload object.
No ReadableStream object is used in the request.
For simple requests, the browser sends the actual request directly with an Origin header. The server (S3) responds with an Access-Control-Allow-Origin header if the origin is allowed. If the header matches the requesting origin, the browser allows the response to be shared with the web page. If not, the browser blocks the response.
Preflight Requests – For all other requests (e.g., PUT, DELETE, or requests with custom headers like Authorization or X-Custom-Header), the browser first sends an OPTIONS request to the server with headers:
- Origin: the requesting origin.
- Access-Control-Request-Method: the HTTP method of the actual request.
- Access-Control-Request-Headers: a comma-separated list of custom headers.
The server must respond with appropriate CORS headers:
- Access-Control-Allow-Origin: the allowed origin (or * for any).
- Access-Control-Allow-Methods: the allowed methods (e.g., GET, PUT, DELETE).
- Access-Control-Allow-Headers: the allowed custom headers.
- Optionally Access-Control-Max-Age: how long the preflight response can be cached (in seconds).
If the preflight response is valid, the browser proceeds with the actual request. Otherwise, the actual request is never sent.
Key Components, Values, Defaults, and Timers
S3 CORS Configuration – In S3, CORS is configured at the bucket level using a JSON document called a CORS rule. Each rule can contain:
- AllowedOrigins: up to one * wildcard or specific origins like https://www.example.com. You can specify multiple origins.
- AllowedMethods: one or more of GET, PUT, POST, DELETE, HEAD.
- AllowedHeaders: specific headers or * to allow all.
- ExposeHeaders: headers that the browser can expose to the client (e.g., ETag, x-amz-request-id). By default, only simple response headers are exposed.
- MaxAgeSeconds: time in seconds that the browser caches the preflight response. Default is 0 (no caching). Maximum is 86400 (24 hours).
Default Values – If no CORS rule exists, S3 will not include any CORS headers in responses, causing the browser to block cross-origin requests. There is no default CORS rule; you must explicitly configure it.
Timers – Preflight caching: the MaxAgeSeconds value controls how long the browser can skip the preflight for subsequent requests. A typical value is 3600 (1 hour).
Configuration and Verification Commands
Using AWS Management Console – Navigate to the S3 bucket → Permissions → Cross-origin resource sharing (CORS). Edit the CORS configuration JSON.
Using AWS CLI – Use put-bucket-cors:
aws s3api put-bucket-cors --bucket my-bucket --cors-configuration file://cors.jsonExample cors.json:
{
"CORSRules": [
{
"AllowedOrigins": ["https://www.example.com"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "x-amz-request-id"],
"MaxAgeSeconds": 3600
}
]
}To retrieve the current CORS configuration:
aws s3api get-bucket-cors --bucket my-bucketTo delete CORS configuration:
aws s3api delete-bucket-cors --bucket my-bucketVerification – Use curl with the -H option to simulate a cross-origin request:
curl -H "Origin: https://www.example.com" -H "Access-Control-Request-Method: GET" -X OPTIONS -I https://my-bucket.s3.amazonaws.com/my-objectCheck the response headers for Access-Control-Allow-Origin.
How CORS Interacts with Related Technologies
CloudFront – When using CloudFront with an S3 origin, CORS configuration can be set either at the S3 bucket level or at the CloudFront distribution level using custom headers. However, the recommended approach is to configure CORS at the S3 bucket and let CloudFront forward the Origin header. If CloudFront caches responses, it must be configured to cache based on the Origin header to avoid serving a cached response from one origin to another. This is done by setting the cache policy to include the Origin header in the cache key.
API Gateway – For REST APIs, CORS must be enabled on the API Gateway resource. The exam often tests the difference between enabling CORS via the console (which creates an OPTIONS method) versus manually adding headers in integration responses.
Lambda@Edge – Can be used to modify CORS headers dynamically at CloudFront edge locations.
Identify the origin mismatch
The web application hosted on `https://app.example.com` makes an XMLHttpRequest to `https://my-bucket.s3.amazonaws.com/data.json`. The browser detects that the target origin (S3 bucket) differs from the source origin (web app). The browser applies the same-origin policy and initiates the CORS mechanism.
Determine request type
The browser classifies the request as simple or non-simple. A GET request with no custom headers is simple. A PUT request with a custom header like `Content-Type: application/json` is non-simple, triggering a preflight. The browser sends an OPTIONS request for non-simple requests.
Send preflight (if needed)
For non-simple requests, the browser sends an HTTP OPTIONS request to the S3 bucket with headers: `Origin: https://app.example.com`, `Access-Control-Request-Method: PUT`, and `Access-Control-Request-Headers: Content-Type`. The S3 bucket receives this request and checks its CORS rules.
S3 evaluates CORS rules
S3 compares the request's `Origin` against the `AllowedOrigins` list in its CORS configuration. It also checks the request method against `AllowedMethods` and request headers against `AllowedHeaders`. If there is a matching rule, S3 responds with appropriate CORS headers. If no rule matches, S3 returns a 403 Forbidden with no CORS headers.
Browser processes preflight response
If the preflight response includes `Access-Control-Allow-Origin` matching the requesting origin, the browser allows the actual request to proceed. If the response is missing or incorrect, the browser blocks the actual request and logs a CORS error. The actual request is never sent.
Send actual request and receive response
For successful preflight, the browser sends the actual PUT request. S3 processes the request and returns the response with CORS headers (e.g., `Access-Control-Allow-Origin`). The browser checks that the response includes the required CORS headers; if so, it shares the response with the web page. If not, the response is blocked.
Scenario 1: Single-Page Application (SPA) Hosted on S3 with API Backend
A company hosts a React SPA on S3 static website hosting (https://myapp.s3-website-us-east-1.amazonaws.com). The SPA makes AJAX calls to a REST API hosted on API Gateway (https://api.example.com). The API Gateway must have CORS enabled. The S3 bucket itself does not need CORS for this scenario because the SPA's JavaScript code is loaded from S3, but the actual API calls go to API Gateway. However, if the SPA also loads assets (images, fonts) from a different S3 bucket, that bucket needs CORS rules. The developer configures CORS on the asset bucket with AllowedOrigins: ["https://myapp.s3-website-us-east-1.amazonaws.com"] and AllowedMethods: ["GET", "HEAD"]. In production, they use CloudFront with custom domain to avoid the S3 website endpoint origin issue.
Scenario 2: Media Hosting for Multiple Domains
A media company hosts video thumbnails in an S3 bucket and serves them to multiple partner websites (e.g., partner1.com, partner2.com). The bucket CORS configuration includes both origins in AllowedOrigins. They also expose custom headers like x-amz-meta-* for metadata. They set MaxAgeSeconds: 86400 to reduce preflight requests. Misconfiguration occurs when a partner uses a subdomain (sub.partner1.com) that is not explicitly listed; the browser blocks the request. The fix is to use a wildcard pattern like https://*.partner1.com if all subdomains are trusted, or list each subdomain explicitly.
Scenario 3: CloudFront with Custom Origin
A company uses CloudFront to accelerate content delivery from an S3 bucket. The S3 bucket is configured as an origin with CORS enabled. CloudFront must forward the Origin header to S3; otherwise, CloudFront might cache a response for one origin and serve it to another. The developer sets a cache policy that includes the Origin header in the cache key. They also set MaxAgeSeconds on the S3 CORS rule to 3600. Common mistake: forgetting to forward the Origin header, leading to intermittent CORS errors when CloudFront serves cached responses without CORS headers. Debugging involves checking CloudFront logs and S3 access logs to see if the Origin header is present.
The DVA-C02 exam tests CORS in the context of S3 and API Gateway. Key objective codes: Domain 1 (Development) – Objective 1.4: Configure CORS for S3. Also appears in Domain 2 (Security) – Objective 2.1: Implement authentication and authorization, where CORS interacts with signed URLs.
Common Wrong Answers:
1. "CORS is configured on the client side." – Candidates confuse CORS headers with client-side code. Reality: CORS is a server-side configuration (S3 bucket policy or API Gateway). The browser enforces it based on server responses.
2. **"Setting AllowedOrigins: * allows any website to access private data."** – While * allows any origin, the data is still protected by bucket policies and IAM. However, * cannot be used with credentials (cookies, authorization headers) or wildcard subdomains; the exam tests this nuance.
3. "CORS errors are caused by the server rejecting the request." – Actually, the browser blocks the response. The server may have responded normally, but the browser discards it if CORS headers are missing or mismatched.
4. "Preflight requests are always sent." – Only for non-simple requests. Many candidates forget the concept of simple requests.
Specific Values and Terms:
- Access-Control-Allow-Origin: can be a specific origin or *. Cannot be * if the request includes credentials.
- Access-Control-Allow-Methods: common values are GET, PUT, POST, DELETE, HEAD.
- Access-Control-Max-Age: maximum 86400 seconds (24 hours).
- AllowedHeaders: use * to allow all headers, but specific headers are needed for security.
- ExposeHeaders: non-simple response headers that the browser exposes to JavaScript.
Edge Cases:
- If the request includes credentials (cookies, HTTP authentication), the Access-Control-Allow-Origin must be a specific origin, not *. Also, the server must include Access-Control-Allow-Credentials: true.
- When using S3 static website hosting, the origin includes the bucket name and region (e.g., http://my-bucket.s3-website-us-east-1.amazonaws.com). That endpoint is different from the REST endpoint (https://my-bucket.s3.amazonaws.com).
- CloudFront can add CORS headers via custom headers or Lambda@Edge, but S3 CORS configuration is simpler.
Elimination Strategy:
- If the question mentions a browser error, look for missing CORS headers.
- If the request uses Authorization header, it's non-simple; expect a preflight.
- If the answer suggests enabling CORS on the client, it's wrong.
- If the answer uses AllowedOrigins: * with credentials, it's wrong.
CORS is a browser mechanism that relaxes same-origin policy; it is configured on the server (S3 bucket) via JSON rules.
Simple requests (GET, HEAD, POST with limited content types) do not trigger a preflight OPTIONS request.
Non-simple requests (PUT, DELETE, or with custom headers) trigger a preflight OPTIONS request that must be answered with CORS headers.
`AllowedOrigins` can be a specific origin or `*`; `*` cannot be used with credentials (cookies, Authorization header).
`MaxAgeSeconds` controls how long the browser caches the preflight response; maximum is 86400 seconds (24 hours).
S3 CORS configuration does not override bucket policies; the bucket must still allow access via IAM or public settings.
When using CloudFront, forward the `Origin` header to S3 and configure cache policy to include it in the cache key.
Common exam scenario: a web app on domain A cannot load resources from S3 bucket B due to missing CORS headers; solution is to add a CORS rule on bucket B with `AllowedOrigins: ["https://domainA"]`.
These come up on the exam all the time. Here's how to tell them apart.
S3 CORS (JSON configuration)
Configured via bucket-level JSON document with `CORSRules` array.
Supports `AllowedOrigins`, `AllowedMethods`, `AllowedHeaders`, `ExposeHeaders`, `MaxAgeSeconds`.
No automatic OPTIONS method creation; S3 handles preflight natively.
Cannot use wildcard for specific subdomains (e.g., `*.example.com` not supported; must list explicitly).
S3 returns CORS headers only if the request's Origin matches a rule.
API Gateway CORS (Console/OpenAPI)
Configured via console (tick boxes) or OpenAPI spec; creates an OPTIONS method.
Supports `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, `Access-Control-Expose-Headers`, `Access-Control-Max-Age`.
Requires explicit OPTIONS method with mock integration or custom integration.
Supports wildcard for origins (e.g., `*.example.com` via regex in some cases).
Headers are added to integration responses; can be overridden per method.
Mistake
CORS is a security feature that prevents cross-origin requests from being sent.
Correct
CORS does not prevent requests from being sent; it prevents the browser from reading the response if the server does not allow it. The request is still sent, but the browser blocks the JavaScript from accessing the response.
Mistake
Setting `AllowedOrigins: *` allows any website to access my S3 bucket data.
Correct
While `*` allows any origin, the bucket still must be publicly readable or the requests must be signed. CORS does not override bucket policies or IAM. It only tells the browser to share the response with the requesting origin.
Mistake
Preflight requests are sent for every cross-origin request.
Correct
Preflight requests are only sent for non-simple requests (e.g., methods other than GET/HEAD/POST, or with custom headers). Simple requests (GET, HEAD, POST with limited content types) do not trigger a preflight.
Mistake
CORS configuration on S3 is the same as on API Gateway.
Correct
S3 uses a JSON configuration with `CORSRules`. API Gateway requires enabling CORS on the resource, which creates an OPTIONS method and adds headers to integration responses. The exam tests both separately.
Mistake
If I use CloudFront with S3, I don't need to configure CORS on S3.
Correct
If CloudFront forwards the `Origin` header to S3, S3 still needs CORS configuration. CloudFront can also add CORS headers via custom headers, but the recommended approach is to configure CORS at the origin.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
A public bucket allows anyone to read objects via direct URL, but the browser still enforces CORS. If your web app is on a different origin, the browser checks for CORS headers from S3. Even if the bucket is public, without a CORS rule that includes your web app's origin, the browser blocks the response. Solution: add a CORS rule on the S3 bucket with your web app's origin.
No, S3 CORS does not support wildcard patterns for subdomains. You must list each origin explicitly (e.g., `https://sub1.example.com`, `https://sub2.example.com`). However, you can use a single wildcard `*` to allow all origins, but that disables credentials.
`AllowedHeaders` specifies which request headers the browser can send (e.g., `Content-Type`, `Authorization`). `ExposeHeaders` specifies which response headers the browser can expose to the client JavaScript (e.g., `ETag`, `x-amz-request-id`). By default, only simple response headers are exposed.
Use `curl` to send an OPTIONS request with the `Origin` and `Access-Control-Request-Method` headers: `curl -H "Origin: https://yourdomain.com" -H "Access-Control-Request-Method: GET" -X OPTIONS -I https://your-bucket.s3.amazonaws.com/object`. Check the response for `Access-Control-Allow-Origin`. You can also use browser developer tools to see the request/response headers.
Yes, signed URLs work with CORS, but the signed URL must be for the same bucket. The CORS rule still applies; the browser checks the response headers. If the signed URL includes credentials (e.g., `AWSAccessKeyId`), the request is considered credentialed, so `AllowedOrigins` cannot be `*`.
Yes, you can configure CloudFront to add CORS headers via custom headers or Lambda@Edge. However, the recommended approach is to configure CORS at the S3 origin and ensure CloudFront forwards the `Origin` header. This avoids caching issues.
If `MaxAgeSeconds` is not set (or set to 0), the browser will send a preflight request for every non-simple request. This increases latency. A typical value is 3600 (1 hour) to reduce overhead.
You've just covered S3 CORS Configuration for Web Apps — now see how well it sticks with free DVA-C02 practice questions. Full explanations included, no account needed.
Done with this chapter?