# S3 CORS Configuration for Web Apps

> Chapter 53 of the Courseiva DVA-C02 curriculum — https://courseiva.com/learn/dva-c02/s3-cors-dva

**Official objective:** 1.4 — Development

## Introduction

CORS (Cross-Origin Resource Sharing) configuration for Amazon S3 in web applications is 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.

## The VIP Club Guest List

Picture a VIP club — the web application on domain B — where entry is restricted to individuals 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.

## Core explanation

### 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.json
```

Example `cors.json`:
```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-bucket
```

To delete CORS configuration:
```
aws s3api delete-bucket-cors --bucket my-bucket
```

**Verification** – 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-object
```
Check 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.

## Real-world context

**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.

## Exam focus

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.

## Step by step

1. **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.
2. **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.
3. **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.
4. **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.
5. **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.
6. **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.

## Comparisons

### S3 CORS (JSON configuration) vs API Gateway CORS (Console/OpenAPI)

**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.

## Common misconceptions

- **Misconception:** CORS is a security feature that prevents cross-origin requests from being sent. **Reality:** 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.
- **Misconception:** Setting `AllowedOrigins: *` allows any website to access my S3 bucket data. **Reality:** 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.
- **Misconception:** Preflight requests are sent for every cross-origin request. **Reality:** 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.
- **Misconception:** CORS configuration on S3 is the same as on API Gateway. **Reality:** 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.
- **Misconception:** If I use CloudFront with S3, I don't need to configure CORS on S3. **Reality:** 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.

## Key takeaways

- 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"]`.

## FAQ

**Why do I get a CORS error even though my S3 bucket is public?**

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.

**Can I use a wildcard in `AllowedOrigins` for subdomains like `*.example.com`?**

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.

**What is the difference between `AllowedHeaders` and `ExposeHeaders`?**

`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.

**How do I test if my S3 CORS configuration is working?**

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.

**Does CORS work with S3 signed URLs?**

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 `*`.

**Can I configure CORS on CloudFront instead of S3?**

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.

**What happens if I don't set `MaxAgeSeconds`?**

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.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/dva-c02/s3-cors-dva
