TABLE OF CONTENTS

Bypass The Browser CORS Mechanism By Using A Proxy

Author Sagi Liba
Sagi Liba on Dec 16, 2021
9 min 🕐

While trying to debug an issue in a deployed AWS development environment at work, I've noticed that the local frontend application is having CORS issues when directing my application requests towards the development environment.

We are currently working on a new project from scratch

Cross-Origin Resource Sharing (CORS)

MDN: Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.

It basically means that the server can decide which origins it would like to share its resources with. Browsers are enforcing the CORS mechanism in order to avoid major security vulnerabilities that could arise without it.

The important thing is that the actual validation of the CORS mechanism is done by the browser.
☝️ Read that again ☝️

To allow a localhost environment to access another origin ( your remote server ) you have to make sure that the relevant access control headers are returned by the server, and that they include your local environment.

For most projects I believe the following solution will be the easiest, directly set your localhost origin as allowed inside the access-control-allow-origin header.

Make sure you put the correct origin, here I've used http://localhost:3000
Copy
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true

If you wish to open your server to any origin you can use an asterisk:

* - open to any origin
Copy
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Now before you run and use that for your dev/staging/prod environments at work, make sure that you are only allowing the CORS headers inside your workplace VPN, otherwise, anyone could still access your server's resources from their local application with the same origin ( http://localhost:port ).

While this is a valid solution, because of necessary implications on other projects we've decided not to go with this solution.

Access-Control-Allow-Credentials

By default, CORS does not include cookies on cross-origin requests. It does so to avoid any possible CSRF vulnerabilities, this makes cookies an active decision that the developer needs to be aware of.

When you send a request from your application you must explicitly give your consent to allow the cookies using the withCredentials property set to true, to give your permission.

Using a Proxy

A proxy is a fancy word for a server that processes your requests and responses for you (in both directions) instead of the actual server you are working with. It sits in between your application and the actual server.

Proxy Server Illustration

As I've said before, the browser is in charge of enforcing the CORS headers, this means that if the request is not going through the browser, and instead it will be sent to a local proxy server you've successfully bypassed the CORS mechanism and you can send your requests without any issues.

I've searched around for existing proxies available and found node-http-proxy, from there I've implemented a simple proxy that forwards my requests to the relevant server and strips any CORS headers when the response comes back. By doing so your local application is not aware that any CORS issues exist and speaks directly with the proxy.

First install the package http-proxy (aka node-http-proxy):

Copy
npm install http-proxy

Set your proxies target server and port:

Copy
const options = {
target: "https://target-server.com", // Proxy target
port: 5050, // Proxy server listening port
};

Initialize a Node server and the proxy server, make sure to send your requests to the proxy:

Copy
const http = require("http");
const httpProxy = require("http-proxy");
const proxy = httpProxy.createProxyServer({});
var server = http.createServer(function (req, res) {
// Passing any request that reaches my server to the proxy
proxy.web(req, res, {
target: options.target,
secure: true, // Verify the SSL Certs
changeOrigin: true, // Set origin of the host header to the target URL
});
});
console.log("listening on port", options.port);
server.listen(options.port);
node-http-proxy allows us to proxy our requests using the web method, you have to pass it your request and response arguments from your local Node server.

webmethod properties:
  • target - which server to forward the requests.
  • secure - When dealing with SSL certificates, this makes sure to verify the SSL certificates.
  • changeOrigin - to "trick" the server into accepting our request, you change the request origin to the target server URL, so it thinks this is an allowed request origin.

Now let's deal with the CORS issues:

  • I'm listening for the proxy event proxyRes which fires when the target server has returned a response.
  • In case of any proxy errors, I'm also listening to the error event.
Copy
...
// Received a response from the target
proxy.on("proxyRes", (proxyRes, req, res) => {
// Next we will override CORS here
});
// Failed to send a request to the target
proxy.on("error", (error, req, res) => {
res.writeHead(500, {
"Content-Type": "text/plain",
});
res.end("Proxy Error: " + error);
});
...

Now I'm going to strip the returned proxy response from any Access-Control headers ( CORS Headers), I'll do so using the function ignoreAccessControlHeaders to filter out those Access-Control headers.

Copy
...
const ignoreAccessControlHeaders = (header) =>
!header.toLowerCase().startsWith("access-control-");
// Received a response from the target
proxy.on("proxyRes", (proxyRes, req, res) => {
proxyRes.headers = Object.keys(proxyRes.headers)
.filter(ignoreAccessControlHeaders)
// Create an object with all the relevant headers
.reduce(
(all, header) => ({ ...all, [header]: proxyRes.headers[header] }),
{}
);
});
...

Finally, I'm making sure the response returned to our client application is allowed with the correct Access-Control headers (allowing CORS).

Copy
...
// Override the response Access-Control-X headers
if (req.headers.origin) {
// Allowing our localhost origin
res.setHeader("access-control-allow-origin", req.headers.origin);
// Allowing CORS to pass cookies
res.setHeader("access-control-allow-credentials", "true");
}
if (req.headers["access-control-request-method"]) {
// Allowing all methods being sent - POST/GET, etc...
res.setHeader(
"access-control-allow-methods",
req.headers["access-control-request-method"]
);
}
// For preflight requests
if (req.headers["access-control-request-headers"]) {
res.setHeader(
"access-control-allow-headers",
req.headers["access-control-request-headers"]
);
}
...

Access-Control-Request-Headers

MDN: The Access-Control-Request-Headersrequest header is used by browsers when issuing a preflight request to let the server know which HTTP headers the client might send when the actual request is made (such as with setRequestHeader()).

The complementary server-side header of Access-Control-Allow-Headerswill answer this browser-side header.

setRequestHeaderis used with XMLHttpRequest, to set the request headers.

Proxy Code

Code Block
Copy
const options = {
target: "https://target-server.com",
port: 5050,
};
const http = require("http");
const httpProxy = require("http-proxy");
const proxy = httpProxy.createProxyServer({});
const ignoreAccessControlHeaders = (header) =>
!header.toLowerCase().startsWith("access-control-");
// Received a response from the target
proxy.on("proxyRes", (proxyRes, req, res) => {
proxyRes.headers = Object.keys(proxyRes.headers)
.filter(ignoreAccessControlHeaders)
// Create an object with all the relevant headers
.reduce(
(all, header) => ({ ...all, [header]: proxyRes.headers[header] }),
{}
);
// Override the response Access-Control-X headers
//
if (req.headers["access-control-request-method"]) {
res.setHeader(
"access-control-allow-methods",
req.headers["access-control-request-method"]
);
}
if (req.headers["access-control-request-headers"]) {
res.setHeader(
"access-control-allow-headers",
req.headers["access-control-request-headers"]
);
}
if (req.headers.origin) {
res.setHeader("access-control-allow-origin", req.headers.origin);
res.setHeader("access-control-allow-credentials", "true");
}
});
// Failed to send a request to the target
proxy.on("error", (error, req, res) => {
res.writeHead(500, {
"Content-Type": "text/plain",
});
res.end("Proxy Error: " + error);
});
var server = http.createServer(function (req, res) {
proxy.web(req, res, {
target: options.target,
secure: true, // Verify the SSL Certs
changeOrigin: true, // Set origin of the host header to the target URL
});
});
console.log("listening on port", options.port);
server.listen(options.port);

Finally, we have our proxy setup, you can run the proxy using:

Copy
// I've named the proxy file "index.js".
node index.js

Proxy using HTTPS

In the next part of the series I'll be going over how to setup your SSL certificates for local development. In case you need the proxy to run using HTTPS, you can learn to create your certificates here:

Using HTTPS In Development By Creating Validated SSL Certificates

Then update the proxy code to use HTTPS:

#1Require the https module:
Copy
const https = require("https");
#2Create https configurations:
Copy
// I'm using the certificates
// that I've given you a link how to create.
const https_options = {
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
};
#3Create a local server using HTTPS:
Copy
// Replace the previous HTTP server.
var server = https.createServer(https_options, function (req, res) {
proxy.web(req, res, {
target: options.target,
secure: true, // Verify the SSL Certs
changeOrigin: true, // Set origin of the host header to the target URL
});
});
console.log("listening on port", options.port);
server.listen(options.port);

Client application

All that is left is to make sure our client application speaks directly to our proxy instead of the actual deployed server.

I've used axios to handle the asynchronous requests. A known best practice is to have a single instance of your API handler to handle your requests.

At that instance, make sure your are using the proxy only for development:

Copy
let serverCall = axios.create({
baseURL:
process.env.NODE_ENV === "development"
? "http://localhost:5050" // port 5050 was defined in our proxy code
: "https://target-server.com",
});

That's it, any request being sent from your application in development should be directed to our proxy (make sure you've started the proxy server) from there it will be sent to the server with the correct origin, it will then receive a response and will change the CORS headers to match our localhost origin, and you've successfully bypassed the CORS mechanism 🎯.

© 2020-present Sagi Liba. All Rights Reserved