Skip to main content
search

The Challenge

Recently, I was working on a cloud-hosted React application that needed to securely upload and download files to and from Amazon S3. The frontend was served through Amazon CloudFront, and the original architecture used AWS Lambda functions to generate pre-signed S3 URLs for file access.

CORS errors — vague, persistent, and resistant to all the usual fixes.

Despite correctly configuring S3 CORS policies, the browser refused to cooperate. I received a mix of 403 errors, “CORS header missing,” and “preflight request didn’t succeed” messages. It became clear that the architecture itself needed a rethink.

What I Discovered

After a lot of testing, debugging, and documentation review, I realized the core issue was how the browser views origin boundaries — and how that interacts with pre-signed S3 URLs vs. CloudFront-hosted frontends.

When using Lambda to return pre-signed URLs for S3, your React app (hosted on https://my-app.example.com) ends up trying to PUT or GET directly from https://my-bucket.s3.amazonaws.com. That’s cross-origin, which means:

  • Browsers initiate preflight (OPTIONS) requests
  • S3 must return the right Access-Control-Allow-* headers
  • Any mismatch, and the whole upload/download flow fails silently or with 403s

The complexity increases when S3 is public-read but still protected by WAF/IP policies, and when CloudFront is only used in front of the app bucket — not the object storage bucket.

The Solution: CloudFront in Front of Everything

The breakthrough came when I added CloudFront in front of both S3 buckets:

  1. One for the React frontend
  2. One for the object storage (certs/files/etc.)

I then:

  • Routed them using multiple origins and behaviors
  • Set index.html as the root object for the frontend
  • Used path patterns (like /certs/*) to handle uploads/downloads
  • Secured both origins behind AWS WAF using IP allowlists
  • Properly configured CORS on S3 and used CORS response headers policies on CloudFront

By doing this, all traffic originated from the same domain — CloudFront became the single origin from the browser’s perspective. That eliminated the need for CORS preflight workarounds and simplified my security model.

Lessons Learned

Here’s what I’d recommend to anyone building a secure, static + storage web app in AWS:

  1. Use CloudFront for all traffic — including file storage, not just your frontend
  2. Avoid direct S3 URLs from the browser unless you’re confident in handling all CORS edge cases
  3. Use behaviors and path-based routing to map multiple buckets under one CloudFront domain
  4. Keep S3 buckets public only behind CloudFront + WAF/IP filtering, not exposed directly
  5. Test with tools like curl – i -X OPTIONS to inspect actual CORS headers during dev
Close Menu