DEV Community

John Detlefs
John Detlefs

Posted on

CORS and SameSite Cookies Got You Down? An Effective Workaround for Browser Security Policies

Of CORS My Request Failed

Like many of my colleagues I haven't ever felt like I really understood what Cross Origin Resource Sharing (CORS) policies accomplish. I feel like when I try to learn more about it, I understand it even less. CORS security policies are like the worlds worst child safety lock. Whenever I have looked at information behind it, the explanations are usually very good at explaining what the moving parts are, but rarely give a good explanation for why CORS policies exist. For more background look at these CORS explanations: MDN, Codecademy, Port Swigger, Auth0

There is so much content to look at, but really for most developers this stuff should be abstracted so you don't have to think about it. I doubt very many of us could give a comprehensive breakdown of what a browser request lifecycle looks like. Really though, you can get a long way without deeply understanding this.

As far as I understand it, CORS security defaults on the browser default to the most restrictive rules, any outbound request from content on a domain that isn't making a request to that same domain will be blocked. This is a good thing. Content in the DOM is inherently mutable, so its possible that bad things could happen after your browser renders your HTML/CSS/Javascript.

Your browser is essentially telling you "Hey, if you want to make this request you better put in the work of lying about where its coming from on your server. Get out of here with your funny business!" Servers responding to requests could do work to maintain whitelists for multiple domains, but whitelists are hard to secure and backend developers are justifiably hesitant to make changes to stuff like that.

At Meshify, we've put a fair amount of resources into using Dockerized NGINX servers in tandem with Create-React-App for most of our applications. As a result of this work we've done we are able to:

  • Share Cookies from authenticated requests between pods in our Kubernetes infrastructure
  • Develop against a local service that will work with our API with parity against production behavior
  • Develop against secure WebSockets locally
  • Develop against Service Workers locally
  • Maintain legacy apps to keep them working with stricter security standards

Before you get really started

  • You'll need a config that builds to a directory, in our case its called "build", and our build script is "yarn build"
  • Install Docker
  • Install Homebrew
  • After installing homebrew, brew install mkcert

Creating the certs and templates

  • mkdir templates in your project's root directory
  • mkdir templates/localcerts
  • mkdir templates/nginx

Add your Dockerfile in your root directory

FROM nginx:stable-alpine
COPY templates/nginx /usr/share/nginx/templates
COPY templates/localcerts /usr/share/nginx/certs
WORKDIR /usr/share/nginx/html
COPY build /usr/share/nginx/html

IMPORTANT: That last line build is whatever the name of your build directory is, copy paste errors can trip you up!

Adding some build scripts

scripts: {
    ...
    "build:docker": "yarn build && docker build -t <WHATEVER_YOU_WANT_TO_NAME_YOUR_CONTAINER> .",
    "mkcert": "mkcert -key-file ./templates/localcerts/key.pem -cert-file ./templates/localcerts/cert.pem admin-react.local *.admin-react.local",
    "start:docker": "cross-env REACT_APP_ENV=development PORT=3009 DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start"
    ...
}

Adding your nginx config

# Added proxy_host and upstream_addr for better view of proxied requests
log_format extended '${D}remote_addr - ${D}remote_user [${D}time_local] ${D}proxy_host - ${D}upstream_addr '
                    '"${D}request" ${D}status ${D}body_bytes_sent '
                    '"${D}http_referer" "${D}http_user_agent"'
                    'rt=${D}request_time uct="${D}upstream_connect_time" uht="${D}upstream_header_time" urt="${D}upstream_response_time"';

access_log /var/log/nginx/access.log extended;

upstream api_server {
  server ${BACKEND_SERVER};
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;
  ssl_certificate /usr/share/nginx/certs/cert.pem;
  ssl_certificate_key /usr/share/nginx/certs/key.pem;
  server_name admin-react.local;

  # redirect server error pages to the static page /50x.html
  #
  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }
  # Create React App Specific
  location /sockjs-node/ {
    proxy_pass ${FRONTEND_URL}/sockjs-node/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade ${D}http_upgrade;
    proxy_set_header Connection "upgrade";
  }
  location /api/<YOUR_WEBSOCKET_ENDPOINT> {
    proxy_http_version 1.1;
    proxy_set_header Upgrade ${D}http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass ${BACKEND_URL}stream;
  }
  # It might need to change depending on what your api url looks like
  location /api/ {
    add_header 'Host' api_server always;
    add_header 'Access-Control-Allow-Origin' "${D}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
    if (${D}request_method = 'OPTIONS') {
      # Tell client that this pre-flight info is valid for 20 days
      add_header 'Access-Control-Allow-Origin' "${D}http_origin" always;
      add_header 'Access-Control-Max-Age' 1728000;
      add_header 'Content-Type' 'text/plain charset=UTF-8';
      add_header 'Content-Length' 0;
      return 204;
    }
    proxy_pass ${BACKEND_URL};
  }
  location / {
    proxy_pass ${FRONTEND_URL};
  }
}

Add your docker-compose file

version: "2.4"
services:
  <WHATEVER YOU WANT YOUR SERVICE NAME TO BE>:
    image: <YOUR DOCKER IMAGE NAME>:latest
    ports:
      - "3006:443"
    environment:
      - D=$$
      - FRONTEND_URL=http://host.docker.internal:3009/
      - BACKEND_SERVER=<YOUR_BACKEND_SERVER_URL_WITHOUT_API>
      - BACKEND_URL=<YOUR_BACKEND_SERVER_URL_WITH_API> 
    command: /bin/sh -c "envsubst < /usr/share/nginx/templates/localhost.conf > /etc/nginx/conf.d/localhost.conf && exec nginx -g 'daemon off;'"

Caveat!

Throughout these examples youll see text like <YOUR_BACKEND_SERVER_URL_WITH_API>. I'm expecting you to substitute these with your own endpoints and text. Trailing slashes can be important. If you're running your app and getting 404's, its possible your API slashes are mismatched with your NGINX config. Be careful!

Getting it all started

  1. Run yarn mkcert in your root directory
  2. Run yarn build:docker
  3. Run yarn start:docker
  4. In a separate window, run docker-compose up
  5. Go to https://localhost:3006 click through the security warnings about self signed certs
Meshify's scheme for interacting with a third party service

Imgur image of Meshify's scheme for interacting with a third party service

In our case, we forward requests matching the string 'periscope' to a standalone service that we made to handle some business logic. The standalone service could also be lambdas or some other endpoint that we own. In this instance we get to use the cookie from an authenticated user in that standalone service to make another request to the API to ensure that user has permissions to read what they are accessing.

My brilliant coworker Danil did most of the heavy lifting getting this setup working smoothly with NGINX. Kubernetes works especially well with this setup, COMMAND and ENVIRONMENT exists in Kubernetes configuration the same way it does here, so there are few changes necessary.

I encourage you to comment here if you have any trouble getting this running and wish you luck in breaking out of the cage that your browser puts you in!

Top comments (1)

Collapse
 
patrickjmcd profile image
Patrick McDonagh

Today I learned about the "proxy": "https://website.com" option in package.json for Create-React-App that handles this really well! create-react-app.dev/docs/proxying...