Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Example Nginx configuration for adding cross-origin resource sharing (CORS) support to reverse proxied APIs
#
# CORS header support
#
# One way to use this is by placing it into a file called "cors_support"
# under your Nginx configuration directory and placing the following
# statement inside your **location** block(s):
#
# include cors_support;
#
# As of Nginx 1.7.5, add_header supports an "always" parameter which
# allows CORS to work if the backend returns 4xx or 5xx status code.
#
# For more information on CORS, please see: http://enable-cors.org/
# Forked from this Gist: https://gist.github.com/michiel/1064640
#
set $cors '';
if ($http_origin ~ '^https?://(localhost|www\.yourdomain\.com|www\.yourotherdomain\.com)') {
set $cors 'true';
}
if ($cors = 'true') {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
# required to be able to read Authorization header in frontend
#add_header 'Access-Control-Expose-Headers' 'Authorization' always;
}
if ($request_method = 'OPTIONS') {
# Tell client that this pre-flight info is valid for 20 days
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}

semusi commented Jun 4, 2014

where should i put this - should it be at the end of /etc/nginx/sites-enabled in the site config

Owner

Stanback commented Jul 10, 2014

@semusi You should place it within the curly braces in the server { ... } block of your configuration, which you will find inside the file(s) in /etc/nginx/sites-enabled/. It shouldn't matter where you place it within the server block.

Edit: You should put it in your location block(s).

If you have multiple server blocks that you want to use CORS with, you can also save this as a separate file in /etc/nginx/cors and within your server blocks, enter "include cors;" which will include the file.

Thanks for this code.

kadamska commented Jan 6, 2015

Is there any workaround to set the headers (and get the content) when the server returns a 4xx or 5xx status code?

I just tried this for an issue I'm having getting a meteor android app connected, but "nginx -t" gives:

2015/06/02 12:31:55 [emerg] 7055#0: "add_header" directive is not allowed here in /etc/nginx/sites-enabled/mysite.com.conf:22

I'm running nginx 1.8.0 on Ubuntu 14.04. Has something changed recently?

fcletov commented Jun 13, 2015

I have a same problem with chrisco 23

ralavay commented Jun 15, 2015

@chrisco23 If you are using Ubuntu then should install nginx-extras package to use more_set_headers config

sudo apt-get install nginx-extras

awesome! thanks!

Thanks for this! For people having trouble saying that the "add_header" directive is not allowed here, move the code inside of the location brackets instead of on the server{} level of your configuration.

ravitx12 commented Feb 9, 2016

Is there a reason why you separated the first 2 if statements? Will it not work if I combine and eliminate the variable?

daaain commented Feb 9, 2016

One painful caveat I just discovered is that nginx (as of 1.9) treats the if block as separate context, so if you have add_header elsewhere in the block where you include this then it won't work. Otherwise great one, much nicer than the header repeating ones Google throws up on the top!

agouriou commented Jun 8, 2016

Thanks for this gist. I forked it for Nginx > 1.9 with support for 4xx and 5xx response and exposition of authorization header. https://gist.github.com/agouriou/735daacf7530675552ff248f319a07d9

marcoacm commented Nov 4, 2016

Does this work if I am running javascript on my page that manipulates the page on the nginx server?

For example:
another = window.open("http://site-on-ngix.com/page.php", "_blank");
another.document.getElementsByName("name")[0].value= "marco";

I keep getting
"Uncaught DOMException: Blocked a frame with origin"

@ghost

ghost commented Nov 4, 2016

Thanks for this! It works great. Something I discovered: When serving up polygons, if they are styled with a fill opacity of 0.0, GetFeatureInfo will not return any data (unless you click the outline of the polygon). Changing style fill opacity to 0.1 fixed this.

Owner

Stanback commented Nov 8, 2016

Thanks for the comments guys, I've updated this gist to include support the "always" parameter for nginx 1.7.5+ so that you can get non-200 responses returned back correctly.

I've also added a line for exposing the authorization header based on @agouriou's fork.

In addition, I removed the proprietary X-Mx-ReqToken header and added directives for caching for pre-flight requests, with a content length set to avoid chunked responses.

fakirpic commented Dec 4, 2016

does it work for both of http and https?

jDeppen commented Dec 9, 2016

Yes, @fakirpic. The 's' is optional because of the question mark
https?

elju commented Jan 18, 2017

This doesn't appear to work if included in a location that uses try_files because of this situation https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/ . Any idea how to get around this?

habovh commented Jan 27, 2017

Correct me if I'm wrong, but you're defining the $cors variable only to use it once, which makes it useless. So you can actually simplify your first if to use line 18 for the conditional.
However if your goal was to use the $cors variable in the second if block (to prevent from returning CORS headers from any origin), you should actually do something like my forked gist: https://gist.github.com/habovh/23506240581b3ca5edc236948808799f

I've added those codes above in my server { . . . } as suggested. I kept getting

nginx: [emerg] "add_header" directive is not allowed here in /etc/nginx/sites-enabled/d.site.com:26
nginx: configuration file /etc/nginx/nginx.conf test failed

Here is my entire configs

server {

    listen 80;
    server_name d.site.com;
    root /home/forge/d.site.com/distributor-application/laravel/public;

    # Enable this line for debugging to see which php.ini get use ... $php --ini
    # root /home/forge/d.site.com/public;


    # FORGE SSL (DO NOT REMOVE!)
    # ssl_certificate;
    # ssl_certificate_key;

    index index.html index.htm index.php;

    charset utf-8;


    set $cors '';
    if ($http_origin ~ '^https?://(localhost|www\.site\.com|www\.d.site\.com|www.\.jsfiddle\.net)') {
            set $cors 'true';
    }

    if ($cors = 'true') {
            add_header 'Access-Control-Allow-Origin' "$http_origin" always;
            add_header 'Access-Control-Allow-Credentials' 'true' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
            # required to be able to read Authorization header in frontend
            #add_header 'Access-Control-Expose-Headers' 'Authorization' always;
    }

    if ($request_method = 'OPTIONS') {
            # Tell client that this pre-flight info is valid for 20 days
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
    }



    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/d.site.com-error.log error;

    error_page 404 /index.php;

    location ~ \.php$ {

        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_read_timeout 600;
       fastcgi_send_timeout 600;
       fastcgi_buffers 16 16k;
       fastcgi_buffer_size 32k;
        fastcgi_index index.php;
        include fastcgi_params;

    }

    location ~ /\.ht {
        deny all;
    }

}

Please let me know what I did wrong !!!

felipekm commented Mar 9, 2017

@bunlongheng you must to put your add_header inside location / {

ejcx commented Apr 3, 2017

Please read this comment if you plan to use the above gist.

The above code contains a very serious vulnerability. Specifically this line:

Vulnerable

if ($http_origin ~ '^https?://(localhost|www\.site\.com|www\.d.site\.com|www.\.jsfiddle\.net)') {

Safe

Note, the $ at the end of the regex, terminating the line. What is actually meant is:

if ($http_origin ~ '^https?://(localhost|www\.site\.com|www\.d.site\.com|www.\.jsfiddle\.net)$') {

Without the extra $, you have essentially turned off all security for your website.

The attacker would do the following to exploit this.

  1. Create a subdomain of their website. For this example, www.site.com.evil.com would be the attackers site.
  2. Lure your logged in users to their evil web page.
  3. Send ajax requests from www.site.com.evil.com to www.site.com with credentials
  4. Because ACAC is set true, and ACAO will reflect the origin header, the AJAX will return successfully.

Please feel free to ping me if you have any questions.

You could use nginx maps in favor of the regex:

map $http_origin $DO_CORS {

  # indicates all map values are hostnames and should be parsed as such
  hostnames;

  # default value
  default 'false';

  # all your domains
  localhost          'true';
  www.yourdomain.com 'true';
  www.yourother.com  'true';
}

This also prevents the attack detailed by @ejcx

Here is my version of doing nginx access control allow origin that avoids some of the duplication.

uicosp commented Jul 10, 2017

if ($request_method = 'OPTIONS') {
        # Tell client that this pre-flight info is valid for 20 days
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
}

This doesn't work, see nginx add_header not working.

There could be several add_header directives. These directives are inherited from the previous level if and only if there are no add_header directives defined on the current level.

I have forked this gist, fix the above issue and make it better (only return NECESSARY CORS headers, e.g. for simple GET\POST request don't need return Access-Control-Allow-Methods header), using it on my production env and everything is ok as far as now. Feel free to let me know if I did something wrong.

Here is my gist https://gist.github.com/uicosp/daa4a4a9ad01cc1130702ac384492b05

Thanks @Stanback give me the idea, also thanks @ejcx to fix the security issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment