Nginx Access-Control-Allow-Origin and CORS

Nginx Access-Control-Allow-Origin

Nginx Access-Control-Allow-Origin header is part of CORS standard (stands for Cross-origin resource sharing) and used to control access to resources located outside of the original domain sending the request.

This standard was created to overcome same-origin security restrictions in browsers, that prevent loading resources from different domains. With the raise of single page apps relying heavily on external API’s and JavaScript apps in general, the need for CORS server configuration is greater than ever. Please note that Fonts ( @font-face within CSS ) and potentially other resources are also affected by same-origin policy.

Ok, so here is the sample of CORS configuration for Nginx:

server {
  listen        80;
  server_name   api.test.com;


  location / {

    # Simple requests
    if ($request_method ~* "(GET|POST)") {
      add_header "Access-Control-Allow-Origin"  *;
    }

    # Preflighted requests
    if ($request_method = OPTIONS ) {
      add_header "Access-Control-Allow-Origin"  *;
      add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
      add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
      return 200;
    }

    ....
    # Handle request
    ....
  }
}

As you can tell by Access-Control-Allow-Origin * – this is wide open configuration, meaning any client will be able to access the resource.

You can list specific hostnames that are allowed to access the server:

add_header "Access-Control-Allow-Origin" "http://test.com, https://example.com"

If you wonder what’s if ($request_method = OPTIONS ) condition, you are not alone. There is slightly confusing concept of Simple and Pre-flight CORS requests (see detailed cors spec).

In the nutshell Simple request is GET, HEAD or POST methods without special headers. In this case request looks like this:

Simle CORS request

and our Nginx config snippet to handle simple requests:

  if ($request_method ~* "(GET|POST)") {
      add_header "Access-Control-Allow-Origin"  *;
  }

If the request involves PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH methods or any special headers not listed for the Simple Request ( see the spec link I gave above ), then it’s treated as Preflighted request. Don’t be scared by fancy words here, in case of preflighted request the client needs to send two requests:

  1. OPTIONS request first to verify what’s allowed. Here is our Nginx config part for that:
    if ($request_method = OPTIONS ) {
      add_header "Access-Control-Allow-Origin"  *;
      add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
      add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
      return 200;
    }
    
  2. Once the client receives the response and checks that original request is allowed. It issues second request with original data.

Here is the diagram to show requests flow:

Preflighted request flow, Nginx access-control-allow-origin

Here are a couple useful CURL command that I use to test the implementation:

curl -s -D - -H "Origin: http://example.com" https://api.example.com/my-endpoint -o /dev/null

You should see Access-Control-Allow-Origin header if everything look good.

To test Preflighted requests, just add -X OPTIONS like this:

curl -s -D - -H "Origin: http://example.com" -X OPTIONS https://api.example.com/my-endpoint -o /dev/null

If you want dive deeper into Nginx access control allow origin and CORS here is excellent post that I already mentioned before – https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS

17 Comments

  1. I have added this as stated by you, but it gave me 404 Not Found error, nginx 1.10 ubuntu 16.04 TLS

    Reply
    1. Can you you paste your configuration as in Nginx is so many little important details that need to seen?

      Reply
  2. Hello Sergey. Thanks for your great work and any guidance you can provide here.

    Building a mini CDN on same server a sub-domain and the CORs started throwing errors for theme/plugins .woff and .ttf. violations.

    As simple as you put it I used the first statement and it stopped the error immediately:
    add_header “Access-Control-Allow-Origin” https://mydomain.com;

    No errors now. good.

    Later that day I realized I couldn’t upload images from a front end uploader due to the:

    XMLHttpRequest cannot load https://cdn.mydomain.com/wp-content/plugins/myplugin/core/lib/upload/my-image-upload.php. Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘https://maindomain.com’ is therefore not allowed access. The response had HTTP status code 500.

    (even though there is the header above which fixed the first errors.

    Then I added as you put above for the pre-flight:

    add_header “Access-Control-Allow-Methods” “GET, POST, OPTIONS, HEAD”;
    add_header “Access-Control-Allow-Headers” “Authorization, Origin, X-Requested-With, Content-Type, Accept”;
    return 200;

    And it swiftly broke all the images (jpg|png) served by the https://sub.samedomain.com throughout the site.

    Everything else I had tried from the Github and other articles that brought me here broke nginx and the sites on that machine. So at least I am one step ahead.

    I am loading these blocks in nginx.my/myfile.conf statements as our nginx.conf is updated to overwrite when new version deployed.

    Everything is https

    so nginx.conf:

    #AUTOMATICALLY GENERATED – DO NO EDIT!

    user www-data www-data;
    pid /var/run/nginx.pid;
    worker_processes 1;
    worker_rlimit_nofile 100000;

    events {
    worker_connections 4096;
    include /etc/nginx.custom.events.d/*.conf;
    }

    http {
    default_type application/octet-stream;

    access_log off;
    error_log /var/log/nginx/error.log crit;

    sendfile on;
    tcp_nopush on;

    keepalive_timeout 20;
    client_header_timeout 20;
    client_body_timeout 20;
    reset_timedout_connection on;
    send_timeout 20;

    types_hash_max_size 2048;

    gzip on;
    gzip_disable “msie6”;
    gzip_proxied any;
    gzip_min_length 256;
    gzip_comp_level 4;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript text/x-js;

    server_names_hash_bucket_size 128;

    include mime.conf;
    charset UTF-8;

    open_file_cache max=100000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    server_tokens off;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    include proxy.conf;
    include fcgi.conf;

    include conf.d/*.conf;
    include /etc/nginx.custom.d/*.conf;
    }

    include /etc/nginx.custom.global.d/*.conf;


    nginx 1.10 Ubuntu 16.04

    Reply
    1. Hey, Stu,

      Can you share configs related to the location where u use add_header “Access-Control-Allow-Origin”? Nginx settings from http section are not very useful and I don’t see any CORS related settings there. Also please use gist or pastebin for big inserts as it’s easier to read.

      Reply
      1. Sorry about that Sergey. Thank you I will get that info when back at my desk tomorrow. Cheers!

        Reply
      2. @@gansbrest:disqus I’ve now got that here – https://gist.github.com/wrrr/5ae2c5afe03f35a007e511b9c66567f5

        I appreciate it Sergey:/

        Reply
        1. When you add this line to the cors.conf:

          `add_header “Access-Control-Allow-Origin” https://mydomain.com;` you essentially adding this header for all requests to all resources on your server ( static / dynamic files ). I would recommend to add it only to resources that needs it (specific locations). But honestly it’s not a big deal, just optimization.

          Now, your main problems comes after you added:

          ““
          add_header “Access-Control-Allow-Methods” “GET, POST, OPTIONS, HEAD”;
          add_header “Access-Control-Allow-Headers” “Authorization, Origin, X-Requested-With, Content-Type, Accept”;
          return 200;
          ““

          You can’t just add those lines ot the cors.conf. If you do that you are essentially replaying with 200 code without a body to all of the requests ( that’s why I think all you images disappeared ). You only need to respond with status 200 to the preflighted OPTIONS request. That’s why there is an if condition and check for the $request_method:

          ““
          if ($request_method = OPTIONS ) {
          add_header “Access-Control-Allow-Origin” https://mydomain.com;
          add_header “Access-Control-Allow-Methods” “GET, POST, OPTIONS, HEAD”;
          add_header “Access-Control-Allow-Headers” “Authorization, Origin, X-Requested-With, Content-Type, Accept”;
          return 200;
          }
          ““

          There is one more detail. You can’t just add this block above to your cors.conf file as Nginx will give you this error: “add_header” directive is not allowed here. You need to add this if block to some location in your code, possibly inside:

          ““
          location / {
          if ($request_method = OPTIONS ) {
          add_header “Access-Control-Allow-Origin” https://mydomain.com;
          add_header “Access-Control-Allow-Methods” “GET, POST, OPTIONS, HEAD”;
          add_header “Access-Control-Allow-Headers” “Authorization, Origin, X-Requested-With, Content-Type, Accept”;
          return 200;
          }
          try_files $uri $uri/ /index.php?$args;
          }
          ““
          Try it. See what you get. The file separation in your config, while good in theory may not be ideal for CORS as they usually location specific, not general sitewide config. (due to the nature of Nginx if handling ).

          Reply
          1. The cors file I included is only called on in this test “separate” from other domains on this machine. The other 2 files exist for WordPress function for clients.

            I will make a separate file to be included as standalone to get the desired result and omit the other includes.

            Sorry I don’t get your syntax (or if it’s truncated) where you put the try_files/…$args: above without closing braces. Can you show me how you would put that whole statement (as you said “inside”?)

            From what I get you are saying it should be possible easily to just make one .conf file “combined”.

            thanks so much again for your expertise.

          2. A bit fussy (as is usual) but that nailed it. This is more about a knowledge catalog for reference for some things you don’t do often, but need in the library. Thanks so much Sergey I will be back to read all your secrets…

          3. Glad you figured it out Stu. Don’t forget to sign up to the newsletter as I have more things coming related to webapps performance 😉

          4. oops. why would https://gist.github.com/wrrr/5ae2c5afe03f35a007e511b9c66567f5#gistcomment-2078017

            throw me – 2017/04/28 14:01:47 [emerg] 4594#4594: unexpected end of file, expecting “}” in /etc/nginx.cors/cors.conf:7

            7 if ($request_method = OPTIONS ) {

          5. I thought you got rid if cors.conf? That sample I gave you is based on your wordpress.conf file. According to the error you missed } somewhere in your configuration. Try removing chunks of code to figure out where you missed it.

          6. I just didn’t rename it for that particular site I used it “as” wordpress.conf and did not “include” both files. (if that makes sense). Meaning your gist would work for that domain instead of wordpress.conf.

          7. Post whole config again if you didn’t figure it out. “unexpected end of file, expecting “}” means you skipped closing curly brace somewhere, most likely in cors.conf

  3. below is my conf file, i am running this website with a uwsgi proxy.
    what i should i add to the conf so that it allows the external access to my jquery requests ?

    server {
    listen 80;
    server_name 10.172.97.146;
    charset utf-8;
    client_max_body_size 75M;

    location / {
    try_files $uri @yourapplication;
    }
    location @yourapplication {
    include uwsgi_params;
    uwsgi_pass unix:/var/www/nsbumobile/nsbumobile_uwsgi.sock;
    }
    }

    Reply
  4. hi there sergey good day! I am still parsing into this cross origin error. the nginx config is running well and that the message request gives 200 code but still the fonts wont take effect in my email template. is there something wrong I am doing with my config. here is my config:
    server {
    listen 8080;
    listen 8081 ssl;
    server_name client.staging.fluidgifts.com client1.staging.fluidgifts.com client2.staging.fluidgifts.com;
    server_tokens off;

    gzip on;
    gzip_disable “msie6”;
    gzip_comp_level 6;
    gzip_min_length 1100;
    gzip_buffers 16 8k;
    gzip_proxied any;
    gzip_types
    text/plain
    text/css
    text/js
    text/xml
    application/x-font-ttf
    application/x-font-opentype
    application/font-woff
    application/font-woff2
    application/vnd.ms-fontobject
    text/javascript
    application/javascript
    application/x-javascript
    application/json
    application/xml
    application/rss+xml
    image/svg+xml;

    etag off;

    #add_header Access-Control-Allow-Origin *;
    #add_header X-Frame-Options crossorigin;

    location ~* \.(eot|ttf|woff|woff2)$ {
    try_files $uri @client;
    # Simple requests
    if ($request_method ~* “(GET|POST)”) {
    add_header “Access-Control-Allow-Origin” *;
    }
    # Preflighted requests
    if ($request_method = OPTIONS ) {
    add_header “Access-Control-Allow-Origin” *;
    add_header “Access-Control-Allow-Methods” “GET, POST, OPTIONS, HEAD”;
    add_header “Access-Control-Allow-Headers” “Authorization, Origin, X-Requested-With, Content-Type, Accept”;
    return 200;
    }
    access_log off;
    }

    location ^~ /.well-known/acme-challenge/ {
    default_type “text/plain”;
    root /usr/share/nginx/html;
    }

    location / {
    try_files $uri @client;
    }

    location @client {
    proxy_pass http://frontend:3000;
    }
    }

    Reply

Leave a Reply