Cloudflare rejecting options requests to R2 bucket and causing CORS to fail in FF

Background:
This question arose while trying to figure out a solution to my question here

The problem:
Preflight OPTIONS requests from Firefox are failing with CORS preflight did not succeed when the request is routed through Cloudflare.

Expected outcome: Firefox should either not send a preflight request at all (like in Chrome), or the request should succeed and the actual content should load.

Facts that may be relevant

  • Two identical OPTIONS requests seem to be sent, which I understand is a possible cause for this. However if this is the case, I am not sure what the cause would be.
  • OPTIONS requests are not appearing in my NGINX logs at all.
  • I have created a WAF rule on Cloudflare that I thought would make sure the OPTIONS request is passed on. That uses the rule (http.request.method eq "OPTIONS") to take the action skip, and ticked every box (including “additional actions”). I may have gotten this very wrong though, please enlighten me if so.
  • Firefox can download the resource by pasting the URL into the browser.
  • Running the failed Firefox request with curl also returned unauthorized:
          <h1>Error 401</h1>
          <h3>This bucket cannot be viewed</h3>
        </div>

        <div>
          <p id="error-title">You are not authorized to view this bucket</p>
          <p>
            This bucket does not exist or is not publicly accessible at this
            URL. Check the URL of the bucket that you’re looking for or contact
            the owner to enable Public access.
          </p>
        </div>

        <div>
          <p id="footer-title">Is this your bucket?</p>
          <p>
            Learn how to enable
            <a
              href="https://developers.cloudflare.com/r2/data-access/public-buckets/"
              >Public Access</a
            >
          </p>
        </div>
      </section>

An example of a preflight request that failed:

OPTIONS /littlebug/edz.zip HTTP/2
Host: arts.example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Access-Control-Request-Method: GET
Access-Control-Request-Headers: range
Referer: https://admin.example.com/
Origin: https://admin.example.com
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Pragma: no-cache
Cache-Control: no-cache
TE: trailers

And the response (note the custom headers that I put in to test Cloudflare):

HTTP/2 403 Forbidden
date: Sun, 17 Mar 2024 22:36:23 GMT
content-type: application/zip
content-length: 16794
vary: Accept-Encoding
cf-cache-status: DYNAMIC
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=kAqV8jlg%2BedhsvfVrb6J3AXMufxZzZEjWZN9k0%2F9dC8VSy8g7%2Fhug6dKnM8XhT9QhXa1C%2BkH06WitFICafs2YHTyUVu3RZuOPCWp1pSh6sFPBqdU3WwDQ3wqNkC4cX%2Fi%2FgI%3D"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
access-control-allow-origin: https://admin.example.com
origin: https://admin.example.com
test-header: Hello World
test-header2: Goodbye world
server: cloudflare
cf-ray: 866076277f75437e-EWR
alt-svc: h3=":443"; ma=86400
X-Firefox-Spdy: h2

And from the same origin, here:

HTTP/2 403 Forbidden
date: Sun, 17 Mar 2024 22:36:23 GMT
content-type: application/zip
content-length: 16794
vary: Accept-Encoding
cf-cache-status: DYNAMIC
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=kAqV8jlg%2BedhsvfVrb6J3AXMufxZzZEjWZN9k0%2F9dC8VSy8g7%2Fhug6dKnM8XhT9QhXa1C%2BkH06WitFICafs2YHTyUVu3RZuOPCWp1pSh6sFPBqdU3WwDQ3wqNkC4cX%2Fi%2FgI%3D"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
access-control-allow-origin: https://admin.example.com
origin: https://admin.example.com
test-header: Hello World
test-header2: Goodbye world
server: cloudflare
cf-ray: 866076277f75437e-EWR
alt-svc: h3=":443"; ma=86400
X-Firefox-Spdy: h2

And for contrast, a GET request for a file of type application/json from the same domain to the same R2 bucket (no preflight was sent):

GET /littlebug/edz.json HTTP/2
Host: arts.example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Origin: https://admin.example.com
Connection: keep-alive
Referer: https://admin.example.com/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Pragma: no-cache
Cache-Control: no-cache
TE: trailers

And the response:

HTTP/2 200 OK
date: Sun, 17 Mar 2024 22:36:23 GMT
content-type: application/json
access-control-allow-origin: https://admin.example.com
etag: W/"12c98e03b4d157e0b0ae977b1aa2443a"
last-modified: Sat, 09 Mar 2024 19:38:09 GMT
vary: Origin, Accept-Encoding
access-control-expose-headers: Content-Encoding,Content-Type,Cache-Control,Content-Length
cf-cache-status: DYNAMIC
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=c2rYtwFcs3wyy58bqYNvrDpWU8rp%2F%2F0mcvA6NjSZ8tCZqiSq%2BSC3aa%2FdoqZzyDvD5Truma2X%2FV%2FDaiXpy79x8vGiLXh0VSoWlRj54DKlp1wDxdVyMJcm3w28kzWqFXpLMgY%3D"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: 866076258da6437e-EWR
content-encoding: br
alt-svc: h3=":443"; ma=86400
X-Firefox-Spdy: h2

Why would Cloudflare return error 401 to the OPTIONS request while allowing the actual file to be downloaded?

1 Like

Well, I got it working. I changed so many settings and configurations that I’m not sure exactly what is important and what isn’t. But these are the configurations as I currently have them and are working.

Nginx

  • My nginx example.com.conf
  server {
      listen 80;
      listen [::]:80;
      server_name admin.example.com;
      return 301 https://$host$request_uri;
  }
 
  server {
      listen 443 ssl;
      listen [::]:443 ssl;
      server_name admin.example.com;
      include /etc/nginx/default.d/*.conf;
      access_log  /var/log/nginx/admin.log;
      error_log  /var/log/nginx/admin.err;
 
 
          location / {
              include proxy_params;
              root   /var/www/html;
              index  index.html index.htm;
          add_header "AllowMethods" "GET,HEAD,OPTIONS" always;
          add_header Access-Control-Allow-Headers "Origin, Authorization, Accept, Content-Type, Range";
          add_header "Access-Control-Max-Age" "3600";
          # Preflighted requests
             if ($request_method = OPTIONS ) {
                add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Content-Length,      User-Agent, Accept, Range" always;
                add_header Content-Length 0;
                add_header Content-Type text/plain;
                return 200;
                 }
             }
 
          error_page   500 502 503 504  /50x.html;
          location = /50x.html {
              root   html;
          }
 
 
  }

Cloudflare settings

  • CORS rules on my R2 bucket:
[
  {
    "AllowedOrigins": [
      "https://admin.example.com"
    ],
    "AllowedMethods": [
      "GET",
      "HEAD"
    ],
    "AllowedHeaders": [
      "Range",
      "Access-Control-Allow-Origin",
      "Authorization",
      "Origin",
      "Content-Type",
      "Accept"
    ],
    "ExposeHeaders": [
      "Content-Encoding",
      "Content-Type",
      "Cache-Control",
      "ETag"
    ],
    "MaxAgeSeconds": 300
  }
]
  • Transform rules on my domain:
    “If” expression:
(http.request.method eq "OPTIONS")

Then…
Add Access-Control-Allow-Headers = Origin, Authorization, Accept, Content-Type, Range

WAF rules on my domain:

If incoming requests match… > expression:
(http.request.method eq "OPTIONS")

Then take action…
Choose action: Skip

WAF components to skip:
I just selected everything. :frowning:

More components to skip
I ticked all those boxes too :frowning: :frowning:

Things I leared along the way:

Firefox is super persnickety about headers.

That means:

  • You can only have one Access-Control-Allow-Origin header.
  • Aside from being bad practice, the oft-recommended use of wildcards will simply be rejected by Firefox
  • Misspelling of any header name will cause preflight requests to fail in Firefox, sometimes not with a very helpful error.
  • CORS errors are often less than helpful.
  • CORS errors can be helpful, but only if certain conditions are met. For example, I had multiple Access-Control-Allow-Origin from the beginning of my attempts, but they were only mentioned in the Network tab after I corrected other things (I have no idea what)

Make sure to check the logs in all the places.

In my case that meant:

  • nginx logs, especially searching for OPTIONS requests.
  • Cloudflare WAF log (go to Security > Events in sidebar).
  • The F in WAF is for firewall. It’s not a mere firewall anymore.
  • In the Cloudflare interface, what most people would call a log is called Events.