Skip to content

Instantly share code, notes, and snippets.

@Stanback
Forked from michiel/cors-nginx.conf
Last active May 10, 2024 12:07
Show Gist options
  • Save Stanback/7145487 to your computer and use it in GitHub Desktop.
Save Stanback/7145487 to your computer and use it in GitHub Desktop.
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;
}
@iver
Copy link

iver commented Apr 9, 2021

@piotrekkr
Copy link

@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

@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

@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

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 ====' ;
}

@RohitSharma2k122
Copy link

RohitSharma2k122 commented Sep 1, 2022

Hi @Stanback @mPanasiewicz

I enabled a CORS in NGINX.Config. But still CORS is not working.

Please let me now where i am missing. Appreciate an input on this.

          server {
          root /usr/share/nginx/www;
          index index.html index.htm;
          location /grafana/ {
          rewrite  ^/grafana/(.*)  /$1 break;
          proxy_set_header Host $http_host;
          proxy_pass http://grafana;
          if ($http_origin ~ '^https?://(example\.com/grafana/\?orgId=1)$') {
                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') 
          {
              add_header 'Access-Control-Allow-Origin' '*';
              #
              # Om nom nom cookies
              #
              add_header 'Access-Control-Allow-Credentials' 'true';
              add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
              
              #
              # Custom headers and headers various browsers *should* be OK with but aren't
              #
              add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified- 
              Since,Cache-Control,Content-Type';
              
              #
              # 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;
          }
          if ($request_method = 'POST') {
              add_header 'Access-Control-Allow-Origin' '*';
              add_header 'Access-Control-Allow-Credentials' 'true';
              add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
              add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified- 
              Since,Cache-Control,Content-Type';
          }
          if ($request_method = 'GET') {
              add_header 'Access-Control-Allow-Origin' '*';
              add_header 'Access-Control-Allow-Credentials' 'true';
              add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
              add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified- 
              Since,Cache-Control,Content-Type';
          }
          }

@piotrekkr
Copy link

@RohitSharma2k122 What exactly is browser saying? Inside error message there should be explanation why CORS request was blocked. Can you post it? Also you can test cors requests with curl command like this (add some headers that are normally send etc):

curl -i -X OPTIONS -H 'Origin: http://frontend-app-host.com' https://backend-host.com

@RohitSharma2k122
Copy link

RohitSharma2k122 commented Sep 5, 2022

Hi @Stanback @mPanasiewicz @piotrekkr

Error is browser after enabled cors
Access to XMLHttpRequest at 'http://example.com/grafana/d/ZmqS29WVk/ade?orgId=1&from=now-2y&to=now&random=1662345172170Mon%20Sep%2005%202022%2008:02:52%20GMT+0530%20(India%20Standard%20Time)' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

Please let me now where i am missing. Appreciate an input on this.

Cors Configuration

    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;
    }

if ($request_method = 'OPTIONS') {
       add_header 'Access-Control-Allow-Origin' '*';
           add_header 'Access-Control-Allow-Credentials' 'true';
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
           add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified- 
           Since,Cache-Control,Content-Type';
           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 = 'POST') {
	add_header 'Access-Control-Allow-Origin' '*';
	add_header 'Access-Control-Allow-Credentials' 'true';
	add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
	add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
 }
 if ($request_method = 'GET') {
	add_header 'Access-Control-Allow-Origin' '*';
	add_header 'Access-Control-Allow-Credentials' 'true';
	add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
	add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}

@piotrekkr
Copy link

Hello @RohitSharma2k122 . Seems like error message indicates problem: Redirect is not allowed for a preflight request . Probably, when browser asks for CORS headers using OPTIONS method it gets redirect response (http => https maybe?).

As I said, you can test those responses by

  1. enabling OPTIONS request in browser dev tools
  2. copy OPTION request as CURL (right click menu in networking tab in dev tools)
  3. Adding -i option to curl command copied before and running it in cli like curl -i .......

It will show you response headers and body of OPTIONS request made by browser.

Btw, you probably should post this question on stackoverflow and not here.

@RohitSharma2k122
Copy link

Thanks you so much @piotrekkr
CORS issue is resolved by adding proxy_set_header Authorization in location tab.

server {
root /usr/share/nginx/www;
index index.html index.htm;
location /grafana/
{
include cors-settings.conf;
rewrite ^/grafana/(.*) /$1 break;
proxy_set_header Host $http_host;
proxy_pass http://grafana;
proxy_set_header Authorization "Basic YWRtaW";
}

@salvationws
Copy link

salvationws commented Sep 8, 2022

Hello. @piotrekkr
Can't make CORS work. Can you help me?
Here is my configuration of Nginx without CORS params.

The backend is HTTP and the Frontend is HTTPS

server {
    listen 80;
    server_name             example-frontend.com www.example-frontend.com;
    return 301              https://example-frontend.com$request_uri;
    }

server {
    listen 443 ssl http2;
    server_name             example-frontend.com www.example-frontend.com;
    # Configuration for SSL/TLS certificates
    ssl_certificate         /etc/letsencrypt/ssl/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/ssl/privkey.pem;
    ssl_stapling            on;
    ssl_session_timeout     1d;
    ssl_session_cache       shared:SSL:50m;
    ssl_session_tickets     off;
    # Disable insecure TLS versions
    ssl_protocols           TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers             EECDH:+AES256:-3DES:RSA+AES:RSA+3DES:!NULL:!RC4;
    # Logs
    access_log              /var/log/nginx/nginx.access.log;
    error_log               /var/log/nginx/nginx.error.log;
    
    
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location /api/ {
        proxy_set_header    X-Forwarded-Host    $host:$server_port;
        proxy_set_header    X-Forwarded-Server  $host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto   $scheme;
        proxy_pass          http://example-backend:8080/;
    }
        
    location / {
        root /app;
        try_files $uri $uri/ /index.html =404;
    }
}

@shlomo7492
Copy link

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

works, the best solution so far!!!

@aamirmalek
Copy link

one silly question: I have nginx ingress which I installed using helm charts. Any idea where to put this annotation or snippet???

Curtly facing issue "Access to XMLHttpRequest at 'https://xxxxx/tenant/dashboard/ml/project?startDate=2021-11-23T09%3A37%3A16-06%3A00&endDate=2022-11-23T09%3A37%3A16-06%3A00&filterby=InProgress&page=1&offset=5' from origin 'https://xxxxxxxxx.io' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."

@mylastore
Copy link

mylastore commented Jan 24, 2023

Justin Fortier

I am trying to do the same as you.
My BE is api.defaultseo.com, and FE defaultseo.com
When I log in from the FE, the refreshToken and Token are set on the BE only
I want to send the tokens to the FE on the response header
so when the FE requests access to a restricted BE route, FE sends the Token & Refresh Token to the BE.

Would you be able to help me fix the above? I also use MyVesta control panel to host the two websites on a droplet with Digital Ocean.

@diegoazh
Copy link

+1 for @mPanasiewicz 👍🏼

@mylastore
Copy link

Justin Fortier

I am trying to do the same as you. My BE is api.defaultseo.com, and FE defaultseo.com When I log in from the FE, the refreshToken and Token are set on the BE only I want to send the tokens to the FE on the response header so when the FE requests access to a restricted BE route, FE sends the Token & Refresh Token to the BE.

Would you be able to help me fix the above? I also use MyVesta control panel to host the two websites on a droplet with Digital Ocean.

I figure this out. It was not related to Nginx

It was on the cookies I just added the cookie attribute: domain: 'defaultseo.com' for example that was my issue.

@bitbay
Copy link

bitbay commented Oct 14, 2023

I'm just familiarizing myself with nginx configurations, but apparently

  • if You use an add_header directive in an if statement, no other add_header from outside the if seems to apply
  • empty strings used in add_header values aren't added/applied to the response

With these in mind, i only could made my case work (different methods allowed for different origins) was defining only variables inside the if statements.

set $allowed_headers '';
set $allowed_methods '';
set $allowed_credentials '';
set $allowed_origin '';
set $type '';
set $length '';

if ( $http_origin ~ "https://put-enabled" ) {
    set $allowed_origin $http_origin;
    set $allowed_methods 'OPTIONS, PUT';
    set $allowed_credentials false;
}

if ( $http_origin ~ "https://delete-enabled" ) {
    set $allowed_headers 'content-type, X-Requested-With';
    set $allowed_origin $http_origin;
    set $allowed_methods 'OPTIONS, DELETE';
    set $allowed_credentials true;
}

if ($request_method = OPTIONS) {
    set $allowed_headers 'content-type';
    set $type 'text/plain';
    set $length 0;
}

add_header Access-Control-Allow-Origin $allowed_origin always;
add_header Access-Control-Allow-Methods $allowed_methods always;
add_header Access-Control-Allow-Headers $allowed_headers always;
add_header Access-Control-Allow-Credentials $allowed_credentials always;
add_header Content-Type $type;
add_header Content-Length $length;

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

if ($request_method = POST) {
    return 404;
}

If You wonder why the POST returns a 404, it's because the navigator (chrome at least) ignores the Access-Control-Allow-Methods for POST requests. 🤷

@Pakbon
Copy link

Pakbon commented Nov 20, 2023

I'm just familiarizing myself with nginx configurations, but apparently

* if You use an `add_header` directive in an `if` statement, no other `add_header` from outside the `if` seems to apply

That's because IF is evil

@freultwah
Copy link

Having enabled the snippet, I scanned my site with nuclei and discovered that the regex '^https?://(localhost|www\.yourdomain\.com|www\.yourotherdomain\.com)' allows for suffixing the domain with anything, as in e.g. www.yourdomain.com.honeypot.com. I amended the regex with a '$' at the end (as in www\.yourotherdomain\.com)$' and it was smooth sailing.

@malithwee
Copy link

mPanasiewicz Works like a charm! thanks

@evtic229
Copy link

Hello @mPanasiewicz & @piotrekkr i saw you master niginx. Thank you for your good job I have a vps server with nginx and tomcat10..17, my frontend is not hosted on my vps. i have deployed my api java and test it throught postman and everything work but while i tried to fetch data from the frontend android and http this issue appear
Access to fetch at 'https://api.domain.com/directives/api/v1/file=' from origin 'https://domain.dev/' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

@Johnson1s
Copy link

y81mcnb3w9p5cep8

@nabtron
Copy link

nabtron commented Apr 1, 2024

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

worked like charm.

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