Post

Solving CORS with an nginx Reverse Proxy

Solving CORS with an nginx Reverse Proxy

This post was migrated from Tistory. You can find the original here.

Terminology before we get started

Domain

domain

Top-Level Domain (TLD): classifies internet addresses.

ex) .com, .org, .net, .edu, .kr, .jp, .uk

Second-Level Domain (SLD): represents the unique name of an organization or website.

ex) google, naver, tistory, youtube

Subdomain: represents a sub-area of the main domain.

Root domain: the domain’s base address, usually made up of SLD + TLD.

SOP (Same-Origin Policy)

A policy that web browsers enforce for security, allowing requests only to resources with the same origin.

Same origin means the protocol, domain, and port are all identical.

Page A: https://example.com

Resource B: https://api.example.com

Even the case above is not the same origin. Even with just a subdomain difference, it’s treated as a different origin.

CORS (Cross-Origin Resource Sharing)

A mechanism designed to work around the Same-Origin Policy.

SOP is a web browser policy, and CORS doesn’t come into play for server-to-server communication.

Forward/Reverse Proxy

   
Characteristicforward proxyreverse proxy
PurposeForwards the client’s requests on its behalfHandles requests on behalf of the server
Main goalAnonymity protection, filtering, bypassing blocksLoad balancing, security hardening, SSL termination, caching
TargetClient (user)Server (service provider)

Think of forward proxy in terms of an intranet.

Solving CORS with an nginx reverse proxy

System configuration

An example system built with Docker

Host: the destination domain

Origin: the source domain

client <=> nginx

nginx serves the static files (html, css, …), and since the client sends requests to itself (nginx), Host and Origin can be made the same.

nginx <=> server, nginx reverse proxy configuration

Example nginx variables

$http_host = 213.111.23.11:9999 (host including the port)

$host = 213.111.23.11 (host excluding the port)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
## nginx.conf

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;
    client_max_body_size 30M;
    ....
    
    # If the request URL and origin differ, you need to explicitly allow the origin instead of using a wildcard, or the policy will block it
    map $http_origin $origin_allowed {
        default 0;
        http://localhost:1111 1;
    }
    
    map $origin_allowed $allow_origin {
        default "*";
        1 $http_origin;
    }

    # http
    server {
        listen 80;
        server_name _;

        include common/_http.conf;
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## _http.conf

location /server {
    include common/_cors-header.conf;
    include common/_cors-options-response.conf;

    # It's fine as long as Host and Origin match, or even if only Host is present
    # When only Host is present, use $http_host (if it's the default port, $http is fine too)
    # Once a DNS is attached, Origin must be present; only when using an IP address is it possible to omit Origin
    rewrite ^/server_prefix/(.*)$ /$1 break;
    proxy_pass http://server:8088;
    proxy_set_header Host $http_host;
    proxy_set_header Origin $scheme://$http_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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
## _cors-header.conf

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

add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
add_header Connection "keep-alive";
add_header Keep-Alive "timeout=20";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "0";
add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers";

## _cors-options-response.conf

# The if block is processed before the global block
if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' $allow_origin always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE, PATCH' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Vary' 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers' always;

    add_header 'Content-Length' 0;
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    add_header 'Access-Control-Max-Age' 7200;
    return 204;
}

add_header applies to the response the client receives; it’s not header configuration that gets forwarded to the server.

It would be different if static file serving happened at a different origin and requests were sent to nginx as a reverse proxy,

but when the client sends a request to itself (nginx), none of the add_header directives above are actually needed to avoid hitting CORS.

proxy_set_header configurations that worked well

The X-aa-bb headers are unrelated to CORS.

1. Setting only Host, without Origin (use $http_host if it’s not the default port)

With a DNS server attached, setting only Host without Origin doesn’t work.

It only worked without Origin when requesting by IP address.

1
proxy_set_header Host $http_host;
  1. Make Origin match Host
    Whether you use $http_host or $host, just apply the same value to both Origin and Host.
1
2
3
4
5
6
7
8
9
10
proxy_set_header Host $http_host;
proxy_set_header Origin $scheme://http_host;

# ==============================================
proxy_set_header Host $host;
proxy_set_header Origin $scheme://host;

# When applying https to nginx and using it as a proxy (while internal communication is http)
proxy_set_header Host $http_host;
proxy_set_header Origin http://$http_host;
  1. Setting Host and having the server’s own allowOrigin configured also works.
1
2
3
4
5
6
proxy_set_header Host $http_host;
proxy_set_header Origin $scheme://abc:5555;

==========================================
proxy_set_header Host $host;
proxy_set_header Origin $scheme://abc:5555;

Excluding the Origin header seems to make it be treated as server-to-server communication, so the CORS mechanism doesn’t run

In every case, it seems that leaving out the Host header from the request causes Spring Boot to return a 400 from some other validation.

(Will check a few more things and update/refine the nginx.conf in this post.)


References

https://backtony.tistory.com/63

This post is licensed under CC BY-NC 4.0 by the author.