Search code examples
nginxamazon-s3firefoxcorscloudflare

CORS rules working for Chrome but not Firefox, but only for some file types


I have some objects stored in Cloudflare R2 storage (.zip and .json files) and they are used to dynamically servce content to users via my website. Not that it should matter to the question at hand, but the .json is providing byte ranges from which content zipped into the zip file is obtained.

In any case, all works well on Chrome and Vivaldi browsers now that I have configured CORS for my R2 bucket (after connecting the bucket to my subdomain).

I have tried a bunch of CORS configurations, but this is my current one. In this one I've thrown in everything my research has pointed to, but not all these rules were needed to make it load in Chrome (and none have helped with Firefox). The subdomain arts.example.com is the subdomain used for my bucket, the page loading that content is example.com

[
  {
    "AllowedOrigins": [
      "http://example.com",
      "https://example.com",
      "http://arts.example.com",
      "https://arts.example.com",
      "http://www.example.com",
      "https://www.example.com"
    ],
    "AllowedMethods": [
      "GET",
      "HEAD",
      "POST"
    ],
    "AllowedHeaders": [
      "Access-Control-Allow-Origin: http://example.com",
      "Access-Control-Allow-Origin: https://example.com",
      "Access-Control-Allow-Origin: http://arts.example.com",
      "Access-Control-Allow-Origin: https://arts.example.com",
      "Access-Control-Allow-Headers: Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With, User-Agent",
      "Content-Type: application/json",
      "Content-Type: application/zip",
      "Content-Type: application/octet-stream",
      "Access-Control-Allow-Credentials: true"
    ],
    "ExposeHeaders": [
      "Content-Encoding",
      "Content-Type",
      "Cache-Control",
      "Content-Length"
    ],
    "MaxAgeSeconds": 300
  }
]

I know similar questions have been asked many times, but I was not able to achieve my goal using them. Perhaps I have missed something, but the answers all seemed to involve CORS headers which I have already set, or the issues were caused by 3rd-party privacy extension. I am not using any privacy extensions.

In Firefox, there are issues with both the preflight request and the main request.

Preflight issues, request

OPTIONS /onion/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://example.com/
Origin: https://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

Preflight issues, response:

HTTP/2 403 Forbidden
date: Mon, 11 Mar 2024 10:52:55 GMT
content-type: text/html
vary: Accept-Encoding
cf-cache-status: DYNAMIC
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=pscazcsNDW4dUrFlJWHnnN8j6gQo6xJY0im4g8voNvUp%2BS2OQ6QUzroL4zs%2BhE6xeN5bbGJZzuS4VHqQwBX7PCt9VeMjjfLCuQ20%2F7OibhJr2NSm1%2FD8yzhjAEJyA2Mf8Lk%3D"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: 862aff6d9ab00f4b-EWR
content-encoding: br
alt-svc: h3=":443"; ma=86400
X-Firefox-Spdy: h2

Main request (no response):

GET /onion/edz.zip undefined
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, identity
Range: bytes=16174665-16250125
Origin: https://example.com
Connection: keep-alive
Referer: https://example.com/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site

For contrast, these are Firefox request and respons headers for a .json file in the same bucket as the problem .zip file.

Preflight: I see no prefight "OPTIONS" request for the .json

Main request:

GET /onion/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://example.com
Connection: keep-alive
Referer: https://example.com/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Pragma: no-cache
Cache-Control: no-cache

Response:

HTTP/2 200 OK
date: Mon, 11 Mar 2024 11:16:58 GMT
content-type: application/json
access-control-allow-origin: https://example.com
etag: W/"01c68f37004a7409fffd99cd8f9d05f6"
last-modified: Sat, 09 Mar 2024 16:34:21 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\/v3?s=XXXXXXXXXXXXXXXXXXXXXXXXX"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: XXXXXXXX-EWR
content-encoding: br
alt-svc: h3=":443"; ma=86400
X-Firefox-Spdy: h2

I suspect that Firefox has a problem with the fact that I am trying to serve content from a .zip file, and there may be nothing to be done about it except to tell users not to use Firefox (not a real option IMO).

Is what I am trying to do impossible?

If not, is there anything else I might try with the CORS settings?

Are there any configurations in nginx (the reverse proxy serving my site) that might make a difference? I have tried adding headers from nginx, but nonthing I tried helped (and I am doubtful that the issue is from Nginx anyway)

Edit: I think I might have found the relevant section of the Cloudflare documentation, I will update as to whether or not this is what I wanted.

Update 1: The documentation linked above seems to be slightly outdated in that the interface has changed, but I am doing my best to follow it. So far all I have managed to do is break things for both Chrome and Firefox. If there is some way to get Firefox to work without adding a whole layer of Cloudflare application in there, that would be nice.

Update 2: I probably should have mentioned that I also have the following in my nginx location block, which if I understand correctly ought to cover anything I should need:

         add_header "Access-Control-Allow-Origin"  "https://example.com" always;
         add_header "Access-Control-Allow-Origin"  "https://arts.example.com" always;
         add_header "Access-Control-Allow-Origin"  "http://example.com" always;
         add_header "Access-Control-Allow-Origin"  "http://arts.example.com" always;
         add_header "AllowMethods" "GET,POST,HEAD,OPTIONS" always;
         add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type" always;

Update 3: I chatted with ChatGPT, which suggested that firewall rules may be the issue. Currently, Cloudflare has firewall rules under a thing called "WAF" (web application firewall). I have been playing with them trying to allow the OPTIONS method, but so far no luck. This is very new to me though, so I am probably not doing it correctly.

Update 4: I suspect it isn't WAF rules (or lack of the right ones) blocking the OPTIONS method, as I just configured them to present a "Managed Challenge" (CAPTCHA), and that is not being presented. So my guess that the problem occurs before the preflight request hits Cloudflare.

Update 5: I tried hosting my content on a different S3 host and got the same pattern of errors in Firefox and success in Chrome. So I am thinking that problem is not on the Cloudflare side after all.

Update 6: When I host the files on a regular Nginx host that is not behind a CDN, I still get the same errors in Firefox but not Chrome. So I am sure it is not a CDN problem. This is the Nginx site config from the web host that is serving the the content without being behind a CDN:

   server {
       listen 80;
       listen [::]:80;
       server_name testing.otherexample.com;
       #    return 301 https://$host$request_uri;
   }
 
   server {
       listen 443 ssl;
       listen [::]:443 ssl;
       server_name testing.otherexample.com;
       include /etc/nginx/default.d/*.conf;
 
           location / {
               include proxy_params;
               root   /var/www/html;
               index  index.html index.htm;
           add_header "Access-Control-Allow-Origin"  "https://admin.example.com" always;
           add_header "AllowMethods" "GET,POST,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-Origin"  "https://example.com" always;
                 add_header "Access-Control-Allow-Origin"  "http://arts.example.com" always;
                 add_header "Access-Control-Allow-Methods" "GET, POST, HEAD, OPTIONS" always;
                 add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Cntent-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;
           }
 
   }

Edit (lucky?) 7: I am now back to thinking it is in fact a CDN issue. This is because I now have cross-site loading of the .zip working when that file is being served directly by nginx. It turns out that Firefox is more persnickety about headers. As you can see in the above nginx conf, I misspelled Content-Type as Cntent-Type. I also have multiple Access-Control-Allow-Origin headers, which I did not realize is a no-no (and which Chrome seems able to tolerate).

But in any case, I still have not achieved my main goal, which is to load zip files from R2 storage into the Ajax library I am using (a modified OpenSeadragon). Now the CORS error is Cors Preflight Did Not Succeed. So my next step is to try to figure out what is causing it not to succeed.

Is it just me, or are a lot of CORS errors obscenely unhelpful in pointing to the source of the problem?

In any case, I am wondering if I should just start a new question that is more specific and not cluttered with multiple stages of troubleshooting.


Solution

  • 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;
                 }
     
              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"
        ],
        "ExposeHeaders": [],
        "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

    Edit: Welp, seems these transform rules were not necessary either, as I have not turned them off and all seems to be well.

    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. :(

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

    Edit: It seems that the WAF rules were actually not necessary. I'm certain they fixed something at some point along the way, but I just disabled them and the content is still loading.

    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.
    • Beware of adding the same header in multiple places.

    Edit: It seems that most the things I thought I was doing to make it work were actually unnecessary. All I can think of is that trying to make the rules work forced me to fix configuration errors in various places (all of which Chrome seems to be fine with and only Firefox objects to), which eventually allowed the CORS rules I started out with to result in a warning that pointed to the multiple allowed origin headers, which then got me to a working config.