How to: CORS — Part 3: By-Pass CORS

Marcin Sodkiewicz
10 min readFeb 15, 2023

--

In this part we will find way to “by-pass” CORS. Please do not use it for serious production workloads. It should be done only as a last resort.

If you just opened this part and have doubts whether you fully understand CORS I strongly recommend to go and read part 1 & part 2

Intro

Ever faced a CORS problem in an API that was out of your control? Are you angry at your backend team for misconfiguring CORS and blocking your work, or are you just looking for some information on how to set up a CORS proxy? If so, let’s dive into the details.

If the API is under your control, it’s easy, otherwise you might feel helpless.

https://cdn-images-1.medium.com/max/800/1*mwAuPk2MSOPSo_2dNXDv6A.jpeg
Helpless dev, pixels on disk

Whether it’s a very busy backend team you’re working with, a third party company that will respond to your email in a couple of days, or worst case, a public API that’s completely out of your control and someone forgot to support CORS, you’re probably going to want to do something about it now.

There are many great public APIs that do not support CORS, so if you would like to build your side-project using those APIs such proxy is something that you might consider.

If you have control of the API or influence to get it added. Do it. It’s the best option. There are lots of libraries available and every serious framework supports it.

Local setup

Plugin
If you only have a CORS problem on your local machine and you want to get rid of it quickly, you can simply select one of the plugins for your browser, but this is not the scenario I want to focus on this time. Keep in mind that enabling such a plugin will compromise the security of your machine and will only work locally on your computer. So watch out for it’s configuration or disable it when it’s not needed.

LocalProxy
You can use a local reverse proxy to deal with this issue. Fo example, you can use nginx just like in this tutorial. Note that if we want to proxy from another origin and route traffic through localhost we have to add Access-Control-Private-Network: true. More details can be found in this article.

location ~ /bands {
proxy_pass https://cfjhrwqpfd3pcgv6kchkdsvevu0scgtu.lambda-url.eu-west-1.on.aws;
add_header "Access-Control-Allow-Origin" "https://sodadev.github.io";
add_header "Access-Control-Allow-Headers" "*";
add_header "Access-Control-Allow-Private-Network" "true";
}

Cloud

I would like to focus on a solution for a situation where you are facing this issue in a dev environment or in your side project where you can’t expect everyone to have such a plugin installed or proxy set and you are using some public API.

You have to be aware that this solution won’t support passing cookies as they are defined for other origin.

Goals

Let’s find a solution which is:

  • Easy to maintain
  • Cheap

CloudFlare

This is a great way to set up a CORS reverse proxy quickly and easily. You can create an account without providing credit card details, which is great.

  • Just setup account by goint to sign-up page.
  • Create your subdomain
  • Click “Continue with free Plan”
  • Go to Workers -> Overview like on the screen below
  • Click “Create a Service” in the top-right corner
  • Create new service
  • Click on “Quick edit” in the top-right corner
  • Now in the edit mode of your worker we can implement some logic like in the Cloudflare’s tutorial that can be found here
  • Update theAPI_URL variable to set the endpoint to which your requests will be proxied. You should also set the PROXY_ENDPOINT variable, which you can use to set up the context path on your proxy. Below is a screenshot of the page that is loaded if the context path is not used.

We can modify the Cloudflare example to something as simple as the snippet below. You can refactor it to support different API's per context or do anything within the limits of workers. Since we can control this code, the sky is the limit.

// Allowed Origin by proxy
const ALLOWED_ORIGIN = 'https://sodadev.github.io'

// The URL for the remote third party API you want to fetch from
// but does not implement CORS
const API_URL = 'https://cfjhrwqpfd3pcgv6kchkdsvevu0scgtu.lambda-url.eu-west-1.on.aws';

// The endpoint you want the CORS reverse proxy to be on
const PROXY_ENDPOINT = '/bands';

const corsHeaders = {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
'Access-Control-Max-Age': '86400',
};

export default {
async fetch(request) {

async function handleRequest(request) {
const url = new URL(request.url);
let apiUrl = `${API_URL}${url.pathname}`;

// Rewrite request to point to API URL. This also makes the request mutable
// so you can add the correct Origin header to make the API server think
// that this request is not cross-site.
request = new Request(apiUrl, request);
request.headers.set('Origin', url.origin);
let response = await fetch(request);

// Recreate the response so you can modify the headers
response = new Response(response.body, response);

// Set CORS headers
response.headers.set('Access-Control-Allow-Origin', corsHeaders['Access-Control-Allow-Origin']);
// Append to/Add Vary header so browser will cache response correctly
response.headers.append('Vary', 'Origin');

return response;
}

async function handleOptions(request) {
return new Response(null, {
headers: {
...corsHeaders,
'Access-Control-Allow-Headers': request.headers.get(
'Access-Control-Request-Headers'
),
},
});
}

const url = new URL(request.url);
if (url.pathname.startsWith(PROXY_ENDPOINT)) {
if (request.method === 'OPTIONS') {
// Handle CORS preflight requests
return handleOptions(request);
} else if (['GET', 'HEAD', 'POST'].indexOf(request.method) !== -1) {
// Handle requests to the API server
return handleRequest(request);
} else {
return new Response(null, {
status: 405,
statusText: 'Method Not Allowed',
});
}
} else {
return new Response(null, {
status: 400,
statusText: 'Bad request',
});
}
},
};

This sounds fine, especially that you don’t even have to specify your payment method. What if you are AWS user and/or you would like to minimize maintenance of even this amount of code?

AWS

Function URLs

Unfortunately, we will end up writing custom code to load this data, and it will only be slightly simpler than the one used in Cloudflare.

Why easier? You don’t have to think about CORS in your lambda because it can be covered by the FunctionURL CORS config. It will take care of preflighting requests for you and add the necessary headers in the response.

What is the worst here? You can set up * as allowed origin and Allow credentials to true and this CORS mechanism will “echo” the origin from the request header into Access-Control-Allow-Origin in the response!!! I know we’re looking at a proxy for dev purposes here, but it’s still something worth noting.

Keep in mind that this API will be public and only option of protection is to use IAM to auth.

API Gateway (RestApi)

You could use API Gateway RestApi with /{proxy+}, but it won’t work.

This is exactly the example that was mentioned at the end of part 2. We can set up CORS and thanks to that it will return a response with CORS headers on preflight. Then the browser will send a request to your proxy API and it won’t have CORS headers in the response — so the response will be blocked by the browser.

API Gateway (HttpApi)

Another option is the HttpApi API Gateway. This time we can define the API proxy with /{proxy+}. We can set up separate routes for different APIs that don’t need to support CORS which we can configure centrally. The way CORS works with HttpApi is really important. It proxies all requests to your API and it only OVERRIDES response headers. This means that if the underlying API doesn’t support OPTONS calls (e.g. returns 405 Method Not Allowed) the preflight call will fail. In other words — our proxy will be useless. Unfortunately there is no way to change the integration response in HttpApi, but response body is not taken into account, so we can only override httpStatus.

When using HttpApi we CAN’T set Access-Control-Allow-Origin as “*” and set Access-Control-Allow-Credentials to “true”

Assuming this is the implementation of our API underneath

const bestBands = {
"1": { "name": "Fontaines D.C." },
"2": { "name": "Idles" },
"3": { "name": "Turnstile" },
"4": { "name": "DIIV" }
}

export const handler = async (event) => {
console.info(event)

if (event.requestContext.http.method === 'OPTIONS') {
return {
statusCode: 405,
body: "Method not supported"
}
}

const response = {
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
rawPath: event.rawPath,
bestBands
}),
};
return response;
};

We will receive this error and our preflight call will fail (if we won’t override http response status):

Access to fetch at has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.

but still it will return CORS headers that we have set.

You can setup such gateway with such simple SAM template:

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Proxy template

Parameters:
3rdPartyUrl:
Type: String
AllowOrigin:
Type: String

Resources:
ProxyApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowHeaders:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
AllowMethods:
- '*'
AllowOrigins:
- !Ref AllowOrigin
AllowCredentials: true
FailOnWarnings: true
DefinitionBody:
openapi: 3.0.1
info:
title: http-proxy-api
version: 2023-01-25T21:11:15Z
paths:
/bands/{proxy+}:
parameters:
- name: "proxy+"
in: "path"
required: true
schema:
type: "string"
x-amazon-apigateway-any-method:
responses:
default:
description: "Default response for ANY /{proxy+}"
x-amazon-apigateway-integration:
responseParameters:
"405":
overwrite:statuscode: "200"
payloadFormatVersion: "1.0"
type: "http_proxy"
httpMethod: "ANY"
uri: !Sub "${3rdPartyUrl}/{proxy}"
connectionType: "INTERNET"
x-amazon-apigateway-importexport-version: "1.0"

And deploy it with command:

sam deploy \
--template-file ./proxy-api.yaml \
--stack-name HTTPAPI-PROXY \
--parameter-overrides \
3rdPartyUrl="https://cfjhrwqpfd3pcgv6kchkdsvevu0scgtu.lambda-url.eu-west-1.on.aws" \
AllowOrigin="https://sodadev.github.io"

Worth to note that your API is public. API Gateway HTTP do not support WAF. So you can add your custom authorizer and/or limit DoW attacks by setting those properties on ProxyApi.

DefaultRouteSettings:
ThrottlingBurstLimit: 20
ThrottlingRateLimit: 10

Margin note: Using CORS Configuration on regular HttpApis

This is also important if you are using HttpApi and CORS configuration. Imagine the following: you are using HttpApi with CORS setup as in the example template above (allow all for some origin). Now at some point you want to have a different setting for one of the endpoints — this will be a problem. If we were to change the preflight handling in our dummy API to allow only calls from google.com and won’t allow credentials with:

if (event.requestContext.http.method === 'OPTIONS') {
return {
statusCode: 200,
headers: {
'Content-Type': 'text/plain',
'Access-Control-Allow-Credentials': false,
'Access-Control-Allow-Origin': "https://google.com"
}
}
}

Despite changes to the handling of OPTIONS in your API, your gateway will still return CORS headers set on HttpApi, not in your code — so: different Origin allowed and Access-Control-Allow-Credentials: true. Please keep this in mind.

CloudFront

We can also use CloudFront to create such proxy. With HttpApi we had two problems that can be solved easily with CloudFront:

Overriding headers
It can be configured thanks to OriginOverride: false that can be set in custom ResponseHeadersPolicy.

Errors returned by API that do not support OPTIONS
This can be resolved, but we have to implement simple CloudFront Function that will return HTTP Status 200 on all OPTIONS request. With CloudFront Edge Functions it’s just few lines of code

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Proxy template

Parameters:
3rdPartyUrl:
Type: String
AllowOrigin:
Type: String

Resources:
ResponseHeaderPolicy:
Type: AWS::CloudFront::ResponseHeadersPolicy
Properties:
ResponseHeadersPolicyConfig:
Name: CorsHeadersReponsePolicy
CorsConfig:
AccessControlAllowCredentials: true
AccessControlAllowHeaders:
Items:
- 'Content-Type'
- 'X-Amz-Date'
- 'Authorization'
- 'X-Api-Key'
- 'X-Amz-Security-Token'
AccessControlAllowMethods:
Items:
- ALL
AccessControlAllowOrigins:
Items:
- !Ref AllowOrigin
OriginOverride: false

PreflightResponder:
Type: AWS::CloudFront::Function
Properties:
Name: PreflightResponder
AutoPublish: true
FunctionConfig:
Comment: "Preflight responder"
Runtime: cloudfront-js-1.0
FunctionCode: |
function handler(event) {
if (event.request.method === 'OPTIONS') {
return {
statusCode: 200,
statusDescription: "OK"
}
}

return event.request
}

ProxyDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
CacheBehaviors:
- AllowedMethods:
- GET
- HEAD
- OPTIONS
- PUT
- PATCH
- POST
- DELETE
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
PathPattern: /bands/*
ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
TargetOriginId: bandsApi
ViewerProtocolPolicy: https-only
FunctionAssociations:
- EventType: viewer-request
FunctionARN: !GetAtt PreflightResponder.FunctionMetadata.FunctionARN
DefaultCacheBehavior:
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
TargetOriginId: bandsApi
ViewerProtocolPolicy: https-only
PriceClass: PriceClass_100
Origins:
- Id: bandsApi
DomainName: !Ref 3rdPartyUrl
CustomOriginConfig:
OriginProtocolPolicy: https-only

What is greatabout this setup is that you can use it along with WAF and cache proxied results on edge.

PS
There is one thing to watch out. If you would like to use Authorization header in your proxy it has to be handled a little bit differently in Cloudfront like in this post.

Proxy cons

  • won’t support cookies
  • not secure as it opens you for SSRF attacks (so again — please do not use such proxy for production)

Conclusion

I hope that at this point you understand how CORS works and how you can by-pass that security mechanism. Just as I wrote on the beginning, it’s just a last resort and it’s not meant for production usage.

Please let me know if you still have some doubts about CORS or there are some other ways/alternatives for those CORS proxies.

--

--