S3 CORS configuration is surprisingly permissive by default, but enabling it for specific origins requires a delicate touch.

Let’s watch an S3 bucket serve a request from a different domain.

First, we need an S3 bucket, let’s call it my-cross-origin-bucket-12345. Inside, we’ll place a file, say image.jpg.

aws s3 cp image.jpg s3://my-cross-origin-bucket-12345/image.jpg

Now, imagine a simple HTML page hosted on a different domain, http://localhost:8000, trying to load this image using JavaScript. Without CORS, the browser will block this request with a "No 'Access-Control-Allow-Origin' header is present" error.

To allow this, we’ll configure the CORS settings on my-cross-origin-bucket-12345. This is done via an XML policy attached to the bucket.

Here’s what a typical CORS configuration looks like:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>http://localhost:8000</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>HEAD</AllowedMethod>
        <ExposeHeader>ETag</ExposeHeader>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
    </CORSRule>
</CORSConfiguration>

This configuration does a few key things:

  • AllowedOrigin: This is the most crucial part. It specifies which origins (protocol, domain, and port) are permitted to make requests to your S3 bucket. In our example, http://localhost:8000 is allowed. You can specify multiple AllowedOrigin elements for different origins. A wildcard * can be used, but this is generally discouraged for security reasons as it allows any origin.
  • AllowedMethod: This lists the HTTP methods (like GET, PUT, POST, DELETE, HEAD) that are allowed from the specified origins. GET and HEAD are common for simply retrieving objects.
  • ExposeHeader: By default, browsers hide most response headers for security. If your JavaScript code needs to access specific headers (like ETag for caching validation), you need to list them here.
  • MaxAgeSeconds: This tells the browser how long it can cache the preflight OPTIONS request. A preflight request is sent by the browser before the actual request (e.g., GET) to check if the server will allow the actual request. Caching this reduces latency for subsequent requests.

You apply this configuration using the AWS CLI:

aws s3api put-bucket-cors --bucket my-cross-origin-bucket-12345 --cors-configuration file://cors-config.xml

Where cors-config.xml contains the XML content shown above.

Once applied, the browser on http://localhost:8000 will receive the Access-Control-Allow-Origin: http://localhost:8000 header in response to its requests to my-cross-origin-bucket-12345, and the image will load successfully.

The browser’s CORS mechanism is a security feature designed to prevent malicious websites from making unauthorized requests to your server on behalf of a logged-in user. S3’s CORS configuration allows you to selectively relax these browser-imposed restrictions for legitimate cross-origin interactions.

The most common mistake people make is using * for AllowedOrigin without fully understanding the security implications, or forgetting to include GET in AllowedMethod when simply trying to read objects.

Another subtlety is that S3’s CORS configuration only controls what the browser is allowed to do. It doesn’t affect direct AWS SDK requests or curl commands, which bypass the browser’s CORS checks entirely.

The next hurdle you’ll likely encounter is handling authenticated requests or more complex scenarios involving preflight OPTIONS requests for methods like PUT or POST, which require specifying AllowedHeaders and Access-Control-Allow-Credentials.

Want structured learning?

Take the full S3 course →