Configure S3 CORS by adding a JSON rule under your bucket's Permissions → Cross-origin resource sharing (CORS) settings. Match your web app's origin, the HTTP methods it uses, and any headers the browser will send.
CORS is not authorization. A browser can require CORS even when a request has valid AWS credentials. Conversely, a matching CORS rule does not grant access to a private object. AWS states that S3 policies and ACLs still apply when CORS is enabled.
A scoped JSON configuration
For an application at https://app.example.com that reads objects and uploads with presigned PUT URLs, start with:
[ { "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["GET", "HEAD", "PUT"], "AllowedHeaders": ["content-type", "x-amz-*"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 300 } ]
Replace the example origin. An origin includes the scheme, host, and non-default port, without a path. http://localhost:5173 is a different origin from your production application; add it explicitly for local development if needed.
AllowedMethods names actual operations, not OPTIONS. If your app only reads files, remove PUT. If it sends an Authorization header or additional signed headers, explicitly permit the headers it actually uses. ExposeHeaders allows browser JavaScript to read selected response headers; it does not authorize object access.
Console versus AWS CLI format
The console accepts the JSON array above. The CLI expects an object containing CORSRules. Save this as cors.json:
{ "CORSRules": [ { "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["GET", "HEAD", "PUT"], "AllowedHeaders": ["content-type", "x-amz-*"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 300 } ] }
Inspect the existing configuration before replacing it; another application may depend on its rules.
aws s3api get-bucket-cors --bucket YOUR_BUCKET aws s3api put-bucket-cors --bucket YOUR_BUCKET --cors-configuration file://cors.json
The update requires permissions to change the bucket's CORS configuration. It does not require disabling S3 Block Public Access.
Test an OPTIONS preflight
Use your bucket's regional REST endpoint and an object path:
curl -i -X OPTIONS 'https://YOUR_BUCKET.s3.YOUR_REGION.amazonaws.com/example.txt' \ -H 'Origin: https://app.example.com' \ -H 'Access-Control-Request-Method: PUT' \ -H 'Access-Control-Request-Headers: content-type'
For a matching rule, inspect Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers in the response. This checks preflight matching; it does not prove an authenticated upload will succeed. Then test the actual browser request with its real credentials or presigned URL. These are expected checks, not a recorded run against your bucket.
Troubleshoot browser failures
| Symptom | Check |
|---|---|
| Origin does not match | Scheme, hostname, port, and the browser's actual Origin header |
| Preflight rejected | Requested method and every Access-Control-Request-Headers entry |
| Preflight succeeds but upload fails | Presigned URL expiry, signer permissions, signed headers, and object key |
| Works with curl but fails in browser | curl does not enforce browser CORS; inspect the browser network panel |
| JavaScript cannot read ETag | Include ETag in ExposeHeaders |
| Old behavior persists | Browser preflight cache and any CDN cache or response-header policy |
For a CloudFront path, inspect how the distribution forwards Origin and preflight headers and how it caches responses. Changing the bucket alone may not change a cached CDN response.
For authorization, see S3 bucket policies with AWS CDK. For hosting a frontend through a private S3 origin, see React deployment with CDK and CloudFront. CORS is needed when the browser crosses origins, not simply because S3 participates in the architecture.
Quick Recap
Does S3 CORS make a bucket public?
No. CORS controls browser cross-origin behavior. Bucket policies, IAM permissions, and S3 Block Public Access continue to govern authorization.
Should OPTIONS appear in AllowedMethods?
No. List the actual operation such as GET or PUT. S3 handles the browser's OPTIONS preflight by matching its requested method and headers against your rule.
Why does an S3 request still return 403 after configuring CORS?
The request may fail origin, method, or header matching, or lack authorization. Check S3's response, the exact origin, bucket policy, credentials, and presigned URL expiry separately.
Comments
Share your thoughts and questions below