nginx config to enable CORS with origin matching - nginx

I've tried to use a very popular config for nginx, which enables CORS and supports origin matching using regular expressions.
Here's my config:
server {
listen 80 default_server;
root /var/www;
location / {
if ($http_origin ~ '^http://(www\.)?example.com$') {
add_header Access-Control-Allow-Origin "$http_origin";
}
# Handling preflight requests
if ($request_method = OPTIONS) {
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}
However, this config must use two conditions: one to match the origin domain name and another one to capture preflight requests. So when the second condition is matched, the headers from the first conditions are not added to the response.
According to the If Is Evil official article, this is an expected behavior for nginx.
If If Is Evil how do I enable CORS in nginx then? Or maybe there is a way to overcome this limitation somehow?

You can try to use map istead of the first if block:
map $http_origin $allow_origin {
~^http://(www\.)?example.com$ $http_origin;
}
map $http_origin $allow_methods {
~^http://(www\.)?example.com$ "OPTIONS, HEAD, GET";
}
server {
listen 80 default_server;
root /var/www;
location / {
add_header Access-Control-Allow-Origin $allow_origin;
add_header Access-Control-Allow-Methods $allow_methods;
# Handling preflight requests
if ($request_method = OPTIONS) {
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}
nginx will refuse to add an empty HTTP headers, so they will be added only if Origin header is present in request and matched this regex.

The only solution I've found so far is a hack to use a variable to aggregate multiple conditions and then match it with only a single if statement, therefore duplicating some directives:
server {
listen 80 default_server;
root /var/www;
location / {
set $cors '';
set $cors_allowed_methods 'OPTIONS, HEAD, GET';
if ($http_origin ~ '^https?://(www\.)?example.com$') {
set $cors 'origin_matched';
}
# Preflight requests
if ($request_method = OPTIONS) {
set $cors '${cors} & preflight';
}
if ($cors = 'origin_matched') {
add_header Access-Control-Allow-Origin $http_origin;
}
if ($cors = 'origin_matched & preflight') {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods $cors_allowed_methods;
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}

A more compliant solution is a bit more involved but does de-duplicate the regex for domain matching, and can be placed into snippets.
I created the file /etc/nginx/snippets/cors-maps.conf which must be included inside the http { ... } block. It contains rules like so:
# always set value to append to Vary if Origin is set
map $http_origin $cors_site_v
{
~. 'Origin';
}
# set site-specific origin header if it matches our domain
map $http_origin $cors_site_origin
{
'~^https://(?:[-a-z\d]+\.)+example\.com$' $http_origin;
}
# validate the options only if domain matched
map '$request_method#$cors_site_origin#$http_access_control_request_method' $cors_site_options
{
# is an allowed method
'~^OPTIONS#.+#(?:GET|HEAD|POST|OPTIONS)$' okay;
# requested an unknown/disallowed method
'~^OPTIONS#.' nope;
}
# set value of Access-Control-Allow-Origin only if domain matched
map '$request_method#$cors_site_origin' $cors_site_acao
{
'~^(?:GET|HEAD|POST)#.' $cors_site_origin;
}
# set value of Access-Control-Allow-Credentials only if Origin was allowed
map $cors_site_acao $cors_site_acac
{
~. 'true';
}
Then /etc/nginx/snippets/cors-site.conf which can be included inside multiple location { ... } blocks:
# only using "if" safely with a "return" as explained in https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/
# return early without access headers for invalid pre-flight, because origin matched domain
if ($cors_site_options = nope)
{
add_header Vary $cors_site_v;
return 204 '';
}
# return early with access headers for valid pre-flight
if ($cors_site_options = okay)
{
add_header Access-Control-Allow-Origin $cors_site_origin;
add_header Access-Control-Allow-Credentials $cors_site_acac;
add_header Vary $cors_site_v;
add_header Access-Control-Allow-Methods 'GET, HEAD, POST, OPTIONS';
# probably overkill, gleaned from others' examples
add_header Access-Control-Allow-Headers 'Accept, Accept-Language, Authorization, Cache-Control, Content-Language, Content-Type, Cookie, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Mx-ReqToken, X-Requested-With';
add_header Access-Control-Max-Age 1728000;
return 204 '';
}
# conditionally set headers on actual requests, without "if", because directive ignored when values are empty strings ("map" default)
add_header Access-Control-Allow-Origin $cors_site_acao;
add_header Access-Control-Allow-Credentials $cors_site_acac;
add_header Vary $cors_site_v;
The # in the values to match aren't special, they simply serve as separators to allow tests with multiple input variables. Extra domains can be added to the map for $cors_site_origin, but would need a bit of tweaking to support domains with different allowed options/headers.

Without getting into the details of your nginx setup, it's not going to work anyway, because the CORS header's you're returning are incorrect...
Specifically:
For preflight (OPTIONS) requests, the following are the only meaningful CORS response headers: Access-Control-Allow Origin, (required), Access-Control-Allow Credentials (optional), Access-Control-Allow-Methods, (required), Access-Control-Allow-Headers, (required) and Access-Control-Max-Age, (optional). Any others are ignored.
For regular (non-OPTIONS) requests, the following are the only meaningful CORS response headers: Access-Control-Allow Origin (required), Access-Control-Allow Credentials (optional) and Access-Control-Expose-Headers (optional). Any others are ignored.
Note those required headers for pre-flight requests - currently you're only passing two of them... Also, note that you don't need to return Access-Control-Allow-Methods for a non-OPTIONS request - it's not 'valid', so will be ignored.
As far as your specific nginx issue goes, I think #Slava Fomin II has the correct-est answer...

Related

How do I set default headers for upstream responses in nginx?

I want to make sure that at least the default header value is always returned from upstream. Even if upstream is unavailable, which causes error 5xx.
Now I have tried these nginx config options:
server {
...
#add_header "Access-Control-Allow-Origin" "*"; №0
#add_header "Access-Control-Allow-Origin" "*" always; №1
#more_set_headers "Access-Control-Allow-Origin: *"; №2
#more_set_headers -s '403 500 502 504 503' "Access-Control-Allow-Origin: *"; №3
location /upstream {
proxy_pass http://localhost:1234;
}
...
}
There are problems with all the options:
№0: Duplicates the header, and in the case of 5xx will not return any.
№1: Duplicates the header
№2: Overrides the upstream header
№3: If the upstream ended with a good http code, but did not return a header, it will not add a header.
I think I'm close to the right solution, but I can't find it.
The map below uses a regex, /.+/, to check if the Access-Control-Allow-Origin header is defined. If so, it assigns its value to the $acao custom variable. Otherwise, it assigns the default value * to $acao;
To avoid duplications, use proxy_hide_header
Finally, add the header using the $acao variable content.
http {
map $upstream_http_access_control_allow_origin $acao {
~.+ $upstream_http_access_control_allow_origin;
default '*';
}
server {
#…
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $acao always;
location /upstream {
proxy_pass http://localhost:1234;
}
}

How to set a cookie in nginx with specific path fragment as a value?

I need to redirect /referral/ID and /referral/ID/ urls (with or without trailing slash) to the root url, but store the ID value in a cookie. I'm trying to do it with the following location section, but this doesn't work.
location /referral {
if ($request_uri ~ "/referral/(.*)(?\/)(.*)") {
add_header Set-Cookie "referral-id=$1;Max-Age=100000";
}
rewrite ^ / permanent;
}
How can I achieve it with nginx?
One of the solutions:
http {
...
map $id $ref_id {
~. "referral=$id;Path=/;Max-Age=100000"; # any non-empty value
# otherwise result of map translation would be an empty string
}
server {
...
location ~ ^/referral(?:/(?<id>[^/]+))? {
add_header Set-Cookie $ref_id;
return 301 /;
}
}
}
If $id variable is empty or null, result of $id to $ref_id translation will be an empty string, and nginx would not set Set-Cookie (or any other HTTP header when an empty value passed as parameter value of add_header directive) at all.

How to use the response header from an upstream host but also provide a default value?

I have an nginx conf which I pair with various applications to act as a reverse proxy.
I would like to enforce some specific response headers but allow the upstream service the freedom to override them.
If the inheritance rules for nginx were different, I would do something like
if ($sent_http_x_content_length = '') {
add_header X-Content-Length "nosniff" always;
}
if ($sent_http_content_type = '') {
add_header Content-Type "text/html" always;
}
But of course, since it's inside the if, the previous if blocks are overwritten.
What is a good workaround that doesn't involve installing the headers-more module?
Ultimately I went with the nginx-plus officially sanctioned more_set_headers community module.
map $sent_http_x_powered_by $resp_x_powered_by {
default $sent_http_x_powered_by;
"" "x powered by is empty";
}
map $sent_http_x_foo $resp_x_foo {
default "x foo by is set";
"" "x foo by is empty";
}
server {
add_header "SomeHeader" "SomeValue";
...
location / {
more_set_headers "X-Powered-By: $resp_x_powered_by";
more_set_headers X-Foo $resp_x_foo;
}
How this works:
more_set_headers will overwrite the headers that you specify with the value provided, e.g. $resp_x_powered_by
The map block will evaluate $resp_x_powered_by by looking at the string in $sent_http_x_powered_by.
If the string is "" then we use the default value of "x powered by is empty". Otherwise, we use the value from the upstream server.
Why I did this:
Initially I tried to use add_headers with map as Raul suggested, but the issue lies in header duplication. If the header is set, then add_header will simply add another line to define the header again. A common workaround is to use proxy_hide_header but that blanks out the value so when we hit the map block, it will always evaluate to "".
As you can see, there is an add_header block outside of the location block which would be overridden by scope inheritance. There are ways to get around it but more_set_headers solves that issue.

How to add headers in nginx only sometimes

I have a nginx proxy to a API server. The API sometimes sets the cache control header. If the API hasnt set the cache control I want nginx to override it.
How do I do that?
I think I want to do something like this, but it doesnt work.
location /api {
if ($sent_http_cache_control !~* "max-age=90") {
add_header Cache-Control no-store;
add_header Cache-Control no-cache;
add_header Cache-Control private;
}
proxy_pass $apiPath;
}
You cannot use if here, because if, being a part of the rewrite module, is evaluated at a very early stage of the request processing, way before proxy_pass is called and the header is returned from the upstream server.
One way to solve your problem is to use map directive. Variables defined with map are evaluated only when they are used, which is exactly what you need here. Sketchily, your configuration in this case would look like this:
# When the $custom_cache_control variable is being addressed
# look up the value of the Cache-Control header held in
# the $upstream_http_cache_control variable
map $upstream_http_cache_control $custom_cache_control {
# Set the $custom_cache_control variable with the original
# response header from the upstream server if it consists
# of at least one character (. is a regular expression)
"~." $upstream_http_cache_control;
# Otherwise set it with this value
default "no-store, no-cache, private";
}
server {
...
location /api {
proxy_pass $apiPath;
# Prevent sending the original response header to the client
# in order to avoid unnecessary duplication
proxy_hide_header Cache-Control;
# Evaluate and send the right header
add_header Cache-Control $custom_cache_control;
}
...
}
Awswer from Ivan Tsirulev is correct but you don't have to use regex.
Nginx uses the first parameter of map as default value automatically so you don't have to add that either.
# Get value from Http-Cache-Control header but override it when it's empty
map $upstream_http_cache_control $custom_cache_control {
'' "no-store, no-cache, private";
}
server {
...
location /api {
# Use the value from map
add_header Cache-Control $custom_cache_control;
}
...
}

How to add http_referer to a cookie?

I was trying something like this:
map $http_referer $setCookie {
default "referrer={$http_referer};Domain=.verbalink.com";
~*test1\.com "referrer=bl;Domain=.verbalink.com;Max-Age=31536000";
}
server{
add_header Set-Cookie $setCookie;
}
But when the cookie is created it has ${http_referer} as the value not the actual http referer. I also tried {$http_referer} and $http_referer but none worked. How can I get the referer into my Cookie?
map $http_referer $setCookie {
default $http_referer;
~*test1\.com "bl;Max-Age=31536000";
}
server{
add_header Set-Cookie "referrer=$setCookie;Domain=.verbalink.com";
}
The resulting value can be a string or another variable. © http://nginx.org/r/map
vs.
value can contain variables © http://nginx.org/r/add_header
If you have only two entries i would map them into some keywords and then in server entry you can If them :) and it should work. So it would look like:
map $http_referer $setCookie {
default "def";
~*test1\.com "test";
}
server{
if ($setCookie == "def"){
add_header Set-Cookie "referrer=$http_referer;Domain=.verbalink.com";
}
if ($setCookie == "test"){
add_header Set-Cookie "referrer=bl;Domain=.verbalink.com;Max-Age=31536000";
}
}

Resources