Skip to content

Instantly share code, notes, and snippets.

Embed
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;
}
@gansbrest
Copy link

gansbrest commented Apr 17, 2017

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

@anchit-nishant
Copy link

anchit-nishant commented Jan 3, 2018

I created a new file in /etc/nginx/conf.d 'cors.conf' with the the below configuration.

Note: My use case was to enable Cors for an nginx reverse proxy which forwards the request to my flask application on docker. My front-end is hosted on AWS S3.

server {
listen 80;

    location / {

        proxy_pass            http://docker;
        proxy_http_version    1.1;
        proxy_set_header    Connection            $connection_upgrade;
        proxy_set_header    Upgrade                $http_upgrade;
        proxy_set_header    Host                $host;
        proxy_set_header    X-Real-IP            $remote_addr;
        proxy_set_header    X-Forwarded-For        $proxy_add_x_forwarded_for;


            set $cors '';
                    if ($http_origin ~ '^https?://(s3\.amazonaws\.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;
                    }


    }
}

@Eternity-Yarr
Copy link

Eternity-Yarr commented Feb 14, 2018

I really don't know why this is working for you, guys. If is broken. https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/ Nginx configuration is not imperative. There is no guarantees that any other block, except for if with return will be executed in case of OPTIONS request.

@Eternity-Yarr
Copy link

Eternity-Yarr commented Feb 14, 2018

If you really need sane CORS functionality, it's better to use some Lua.

@danielmotaleite
Copy link

danielmotaleite commented Jun 6, 2018

save your soul and use the lua below!

https://github.com/detailyang/lua-resty-cors

@mmontag
Copy link

mmontag commented Sep 28, 2018

Please be aware that this will allow your resources to be accessed cross-origin by anybody with a domain name like http://localhost-hacker.biz or https://localhost.blackhat.org. Add a $ to that regex...

@datlife
Copy link

datlife commented Oct 7, 2018

My complete script that works both on my website and localhost:

  • Avoided if, mentioned in ifisevil
  • Fixed regex vulnerability, pointed out by @ejcx above.

In this script, my server is blog.mywebsite.com. It enabled CORS mywebsite.com and localhost to access requested resource.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name blog.mywebsite.com;
    root /var/www/ghost/system/nginx-root;

    ssl_certificate /etc/letsencrypt/blog.mywebsite.com/fullchain.cer;
    ssl_certificate_key /etc/letsencrypt/blog.mywebsite.com/blog.mywebsite.com.key;
    include /etc/nginx/snippets/ssl-params.conf;

    set $cors_origin "";
    set $cors_cred   "";
    set $cors_header "";
    set $cors_method "";

    if ($http_origin ~ '^https?://(localhost|mywebsite\.com)$') {
            set $cors_origin $http_origin;
            set $cors_cred   true;
            set $cors_header $http_access_control_request_headers;
            set $cors_method $http_access_control_request_method;
    }

    add_header Access-Control-Allow-Origin      $cors_origin;
    add_header Access-Control-Allow-Credentials $cors_cred;
    add_header Access-Control-Allow-Headers     $cors_header;
    add_header Access-Control-Allow-Methods     $cors_method;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:2368;
    }

    location ~ /.well-known {
        allow all;
    }
    client_max_body_size 50m;
}

@Zulko
Copy link

Zulko commented Oct 26, 2018

Thanks very much guys ! this code plus @RubenHoms 's comment worked like a charm.

@control-panel
Copy link

control-panel commented Nov 6, 2018

Hello guys!

We have got working config only with the following trick:

`

if ( $request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|PATCH|DELETE)$ ) {
    return 444;
}
    
set $origin $http_origin;

if ($origin !~ '^https?://(subdom1|subdom2)\.yourdom\.zone$') {
    set $origin 'https://default.yourdom.zone';
}

if ($request_method = 'OPTIONS') {

    add_header 'Access-Control-Allow-Origin' "$origin" always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Accept, Authorization' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    add_header Access-Control-Max-Age 1728000;
    add_header Content-Type 'text/plain charset=UTF-8';
    add_header Content-Length 0;
    return 204;
}
    
if ($request_method ~ '(GET|POST|PATCH|PUT|DELETE)') {
    add_header Access-Control-Allow-Origin "$origin" always;
    add_header Access-Control-Allow-Methods 'GET, POST, PATCH, PUT, DELETE, OPTIONS' always;
    add_header Access-Control-Allow-Headers 'Content-Type, Accept, Authorization' always;
    add_header Access-Control-Allow-Credentials true always;
}

`

if block can be implemented with map
may be it can help somebody

@torian257x
Copy link

torian257x commented Dec 12, 2018

@Radiergummi

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

can you expand a bit more in detail? e.g. how to use that map?

@aftabnaveed
Copy link

aftabnaveed commented Jan 8, 2019

save your soul and use the lua below!

https://github.com/detailyang/lua-resty-cors

I get this error

sudo opm install detailyang/lua-resty-cors
Package lua-resty-cors-0.2.1.5 already installed.
Error:

failed to run header_filter_by_lua*: header_filter_by_lua:2: module 'lib.resty.cors' not found:
        no field package.preload['lib.resty.cors']
        no file '/usr/local/openresty/site/lualib/lib/resty/cors.ljbc'
        no file '/usr/local/openresty/site/lualib/lib/resty/cors/init.ljbc'
        no file '/usr/local/openresty/lualib/lib/resty/cors.ljbc'
        no file '/usr/local/openresty/lualib/lib/resty/cors/init.ljbc'
        no file '/usr/local/openresty/site/lualib/lib/resty/cors.lua'
        no file '/usr/local/openresty/site/lualib/lib/resty/cors/init.lua'
        no file '/usr/local/openresty/lualib/lib/resty/cors.lua'
        no file '/usr/local/openresty/lualib/lib/resty/cors/init.lua'
        no file './lib/resty/cors.lua'
        no file '/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/lib/resty/cors.lua'
        no file '/usr/local/share/lua/5.1/lib/resty/cors.lua'
        no file '/usr/local/share/lua/5.1/lib/resty/cors/init.lua'
        no file '/usr/local/openresty/luajit/share/lua/5.1/lib/resty/cors.lua'
        no file '/usr/local/openresty/luajit/share/lua/5.1/lib/resty/cors/init.lua'
        no file '/usr/local/openresty/site/lualib/lib/resty/cors.so'
        no file '/usr/local/openresty/lualib/lib/resty/cors.so'
        no file './lib/resty/cors.so'
        no file '/usr/local/lib/lua/5.1/lib/resty/cors.so'
        no file '/usr/local/openresty/luajit/lib/lua/5.1/lib/resty/cors.so'
        no file '/usr/local/lib/lua/5.1/loadall.so'
        no file '/usr/local/openresty/site/lualib/lib.so'
        no file '/usr/local/openresty/lualib/lib.so'
        no file './lib.so'
        no file '/usr/local/lib/lua/5.1/lib.so'
        no file '/usr/local/openresty/luajit/lib/lua/5.1/lib.so'
        no file '/usr/local/lib/lua/5.1/loadall.so'

@slavafomin
Copy link

slavafomin commented Jan 22, 2019

Thank you for the snippet and all the comments, it's very useful.

@Radiergummi I really like the idea with maps. Is it possible to use wildcard domains there or it could contain only a static list of domain names?

@Stanback Why do you use the following block in preflight requests?

add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;

Are nginx defaults not enough for this?

@slavafomin
Copy link

slavafomin commented Jan 22, 2019

Beware: multiple condition blocks are not going to match simultaneously!

I can't make multiple if statements to work correctly in my nginx configuration.
When the last if statement is matched for the preflight request the headers from the main if block are not added to the response.
And this is an expected behavior according to this official article: If Is Evil.

Have you actually managed to make the both condition blocks apply?

@slavafomin
Copy link

slavafomin commented Jan 22, 2019

Here's the working config, I've managed to implement.

And here's my question on Stack Overflow regarding this issue.

@mPanasiewicz
Copy link

mPanasiewicz commented Jan 30, 2019

My solution is:

map $http_origin $cors_origin_header {
    default "";
    "~(^|^http:\/\/)(localhost$|localhost:[0-9]{1,4}$)" "$http_origin";
    "~^https://test-.-dev.example.pl$" "$http_origin"; # https://test-7-dev.example.pl
    "https://test.example.com" "$http_origin";
}

map $http_origin $cors_cred {
    default "";
    "~(^|^http:\/\/)(localhost$|localhost:[0-9]{1,4}$)" "true";
    "~^https://test-.-dev.example.pl$" "true"; # https://test-7-dev.example.pl
    "https://test.example.com" "true";
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    include ssl/wildcard;

    add_header Access-Control-Allow-Origin $cors_origin_header always;
    add_header Access-Control-Allow-Credentials $cors_cred;
    add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
    add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";

    if ($request_method = 'OPTIONS' ) {
      return 204 no-content;
    }

return 204 there is because this is configuration for load-balancer, and I don't want to send OPTIONS into certain host after load-balancer. If you haven't load-balancer than you can remove this line
I hope, it helps someone

@jfortier
Copy link

jfortier commented Mar 11, 2019

My complete script that works both on my website and localhost:

  • Avoided if, mentioned in ifisevil
  • Fixed regex vulnerability, pointed out by @ejcx above.

In this script, my server is blog.mywebsite.com. It enabled CORS mywebsite.com and localhost to access requested resource.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name blog.mywebsite.com;
    root /var/www/ghost/system/nginx-root;

    ssl_certificate /etc/letsencrypt/blog.mywebsite.com/fullchain.cer;
    ssl_certificate_key /etc/letsencrypt/blog.mywebsite.com/blog.mywebsite.com.key;
    include /etc/nginx/snippets/ssl-params.conf;

    set $cors_origin "";
    set $cors_cred   "";
    set $cors_header "";
    set $cors_method "";

    if ($http_origin ~ '^https?://(localhost|mywebsite\.com)$') {
            set $cors_origin $http_origin;
            set $cors_cred   true;
            set $cors_header $http_access_control_request_headers;
            set $cors_method $http_access_control_request_method;
    }

    add_header Access-Control-Allow-Origin      $cors_origin;
    add_header Access-Control-Allow-Credentials $cors_cred;
    add_header Access-Control-Allow-Headers     $cors_header;
    add_header Access-Control-Allow-Methods     $cors_method;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:2368;
    }

    location ~ /.well-known {
        allow all;
    }
    client_max_body_size 50m;
}

Your solution more or less worked for me using nginx version 1.12.2 Not sure which version of nginx you were using, but I couldn't run add_headers in my server block, but could in my location block which worked fine. Thanks.

@blthayer
Copy link

blthayer commented Jun 3, 2019

@mPanasiewicz's solution worked perfectly for me (so far). Thank you!

@rafi
Copy link

rafi commented Jul 24, 2019

Thanks @mPanasiewicz - your solution is perfect. We ran into many issues with if, it's really evil.

@archy-bold
Copy link

archy-bold commented Sep 11, 2019

Another 👍 for @mPanasiewicz

@iki
Copy link

iki commented Sep 15, 2019

Thanks for inspiration. Here's what we use with added exposed headers: https://gist.github.com/iki/1247cd182acd1aa3ee4876acb7263def

@natanshalva
Copy link

natanshalva commented Jul 18, 2020

@mPanasiewicz - your solution works for me. Thanks!

@robe007
Copy link

robe007 commented Sep 8, 2020

@mPanasiewicz's solution worked perfectly for me (so far). Thank you!

Me too here 👍. Thanks @mPanasiewicz. Your solution works like a charm.

@zffocussss
Copy link

zffocussss commented Apr 2, 2021

My complete script that works both on my website and localhost:

  • Avoided if, mentioned in ifisevil
  • Fixed regex vulnerability, pointed out by @ejcx above.

In this script, my server is blog.mywebsite.com. It enabled CORS mywebsite.com and localhost to access requested resource.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name blog.mywebsite.com;
    root /var/www/ghost/system/nginx-root;

    ssl_certificate /etc/letsencrypt/blog.mywebsite.com/fullchain.cer;
    ssl_certificate_key /etc/letsencrypt/blog.mywebsite.com/blog.mywebsite.com.key;
    include /etc/nginx/snippets/ssl-params.conf;

    set $cors_origin "";
    set $cors_cred   "";
    set $cors_header "";
    set $cors_method "";

    if ($http_origin ~ '^https?://(localhost|mywebsite\.com)$') {
            set $cors_origin $http_origin;
            set $cors_cred   true;
            set $cors_header $http_access_control_request_headers;
            set $cors_method $http_access_control_request_method;
    }

    add_header Access-Control-Allow-Origin      $cors_origin;
    add_header Access-Control-Allow-Credentials $cors_cred;
    add_header Access-Control-Allow-Headers     $cors_header;
    add_header Access-Control-Allow-Methods     $cors_method;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:2368;
    }

    location ~ /.well-known {
        allow all;
    }
    client_max_body_size 50m;
}

Your solution more or less worked for me using nginx version 1.12.2 Not sure which version of nginx you were using, but I couldn't run add_headers in my server block, but could in my location block which worked fine. Thanks.

add_header can work in http context .see http://nginx.org/en/docs/http/ngx_http_headers_module.html

@iver
Copy link

iver commented Apr 9, 2021

@piotrekkr
Copy link

piotrekkr commented Jul 22, 2021

@mPanasiewicz Your solution works quite good but I preferred solution from @slavafomin since it does not send cors headers when there is no cors request (no Origin header in request)

@piotrekkr
Copy link

piotrekkr commented Jul 23, 2021

Ok so after some more testing I still had some errors concerning not allowed headers. After reading https://fetch.spec.whatwg.org/#http-cors-protocol and based on previous comments, I've created this configuration below. It works for me because:

  1. I needed to allow all origins
  2. I don't care about headers that are sent
  3. I needed to allow credentials (to send cookies in CORS requests)
  4. I want to send CORS headers only for CORS requests
  5. Access-Control-Allow-Origin cannot be * because CORS disallow this with credentials enabled
    location ~ ^/index\.php(/|$) {

        set $cors '';

        # Match anything not empty
        if ($http_origin ~ '.+') {
            set $cors 'origin_matched';
        }

        # Preflight requests
        if ($request_method = OPTIONS) {
            set $cors '${cors} & preflight';
        }

        # standard CORS request
        if ($cors = 'origin_matched') {
            # for standard CORS requests only those headers are required
            add_header Access-Control-Allow-Origin $http_origin always;
            add_header Access-Control-Allow-Credentials 'true' always;
        }

        # preflight CORS request
        if ($cors = 'origin_matched & preflight') {
            # standard CORS headers are required
            add_header Access-Control-Allow-Origin $http_origin always;
            add_header Access-Control-Allow-Credentials 'true' always;

            # additional headers are required and need to match exactly what was sent in CORS request
            add_header Access-Control-Allow-Methods $http_access_control_request_method always;
            add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
            add_header Content-Type text/plain;
            add_header Content-Length 0;
            return 204;
        }

        fastcgi_pass ${NGINX_FASTCGI_PASS};
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param REQUEST_URI $uri;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS off;
    }

@mPanasiewicz
Copy link

mPanasiewicz commented Jul 23, 2021

@piotrekkr Maybe you're right but there is one big disadventage which is described here:
nginx if is evil My configuration avoids the if conditions. But feel free to decide what suits best for you ;-)

@piotrekkr
Copy link

piotrekkr commented Jul 25, 2021

@mPanasiewicz Yeah I've read about this but as they wrote:

It is important to note that the behaviour of if is not inconsistent, given two identical requests it will not randomly fail on one and work on the other, with proper testing and understanding ifs ‘’‘can’‘’ be used. The advice to use other directives where available still very much applies, though.

and yes it works for me so I'll stick to this :)

A mystery for me is why for so many years, there is no easy, build in solution in Nginx to handle this?

@piotrekkr
Copy link

piotrekkr commented Jul 25, 2021

Ok, I've played a little with nginx config and I think I've managed to avoid if in location block. Effect is similar to previous config. This is my new config:

server {

    set $cors_credentials '';
    set $cors_content_type '';
    set $cors_content_length '';

    if ($http_origin ~ '.+') {
        set $cors_credentials 'true';
    }

    if ($request_method = OPTIONS) {
        set $cors_content_type 'text/plain';
        set $cors_content_length '0';
    }

    # empty header will not be added
    add_header Access-Control-Allow-Origin $http_origin always;
    add_header Access-Control-Allow-Credentials $cors_credentials always;
    add_header Access-Control-Allow-Methods $http_access_control_request_method always;
    add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
    add_header Content-Type $cors_content_type;
    add_header Content-Length $cors_content_length;

    if ($request_method = OPTIONS) {
        return 204;
    }

    location / {
        return 200 '==== OK ====' ;
    }
}

@athompson-paa
Copy link

athompson-paa commented Oct 29, 2021

I'm currently testing as an ingress annotation, should work fine.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/server-snippet: |

set $cors_credentials '';
set $cors_content_type '';
set $cors_content_length '';

if ($http_origin ~ '.+') {
    set $cors_credentials 'true';
}

if ($request_method = OPTIONS) {
    set $cors_content_type 'text/plain';
    set $cors_content_length '0';
}

# empty header will not be added
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials $cors_credentials always;
add_header Access-Control-Allow-Methods $http_access_control_request_method always;
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
add_header Content-Type $cors_content_type;
add_header Content-Length $cors_content_length;

if ($request_method = OPTIONS) {
    return 204;
}

location / {
    return 200 '==== OK ====' ;
}

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