Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Target restriction #111

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,27 +89,32 @@ proxy requests. The following options are supported:
* function `getProxyForUrl` - If set, specifies which intermediate proxy to use for a given URL.
If the return value is void, a direct request is sent. The default implementation is
[`proxy-from-env`](https://github.com/Rob--W/proxy-from-env), which respects the standard proxy
environment variables (e.g. `https_proxy`, `no_proxy`, etc.).
* array of strings `originBlacklist` - If set, requests whose origin is listed are blocked.
environment variables (e.g. `https_proxy`, `no_proxy`, etc.).
* array of strings `originBlacklist` - If set, requests whose origin is listed are blocked.
Example: `['https://bad.example.com', 'http://bad.example.com']`
* array of strings `originWhitelist` - If set, requests whose origin is not listed are blocked.
* array of strings `originWhitelist` - If set, requests whose origin is not listed are blocked.
If this list is empty, all origins are allowed.
Example: `['https://good.example.com', 'http://good.example.com']`
* array of strings `targetBlacklist` - If set, requests whose target is listed are blocked.
Example: `['https://bad.example.com', 'http://bad.example.com']`
* array of strings `targetWhitelist` - If set, requests whose target is not listed are blocked.
If this list is empty, all targets are allowed.
Example: `['https://good.example.com', 'http://good.example.com']`
* function `checkRateLimit` - If set, it is called with the origin (string) of the request. If this
function returns a non-empty string, the request is rejected and the string is send to the client.
* boolean `redirectSameOrigin` - If true, requests to URLs from the same origin will not be proxied but redirected.
The primary purpose for this option is to save server resources by delegating the request to the client
(since same-origin requests should always succeed, even without proxying).
* array of strings `requireHeader` - If set, the request must include this header or the API will refuse to proxy.
Recommended if you want to prevent users from using the proxy for normal browsing.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo the removal of every two spaces at the end ( ). These are necessary to force a line break. After your line the Example: ... is not on a separate line any more.

* array of strings `requireHeader` - If set, the request must include this header or the API will refuse to proxy.
Recommended if you want to prevent users from using the proxy for normal browsing.
Example: `['Origin', 'X-Requested-With']`.
* array of lowercase strings `removeHeaders` - Exclude certain headers from being included in the request.
* array of lowercase strings `removeHeaders` - Exclude certain headers from being included in the request.
Example: `["cookie"]`
* dictionary of lowercase strings `setHeaders` - Set headers for the request (overwrites existing ones).
* dictionary of lowercase strings `setHeaders` - Set headers for the request (overwrites existing ones).
Example: `{"x-powered-by": "CORS Anywhere"}`
* number `corsMaxAge` - If set, an Access-Control-Max-Age request header with this value (in seconds) will be added.
* number `corsMaxAge` - If set, an Access-Control-Max-Age request header with this value (in seconds) will be added.
Example: `600` - Allow CORS preflight request to be cached by the browser for 10 minutes.
* string `helpFile` - Set the help file (shown at the homepage).
* string `helpFile` - Set the help file (shown at the homepage).
Example: `"myCustomHelpText.txt"`

For advanced users, the following options are also provided.
Expand Down Expand Up @@ -142,6 +147,13 @@ export CORSANYWHERE_WHITELIST=https://example.com,http://example.com,http://exam
node server.js
```

Similarly, to run a CORS Anywhere server that proxies only to example.com sites on port 8080, use:
```
export PORT=8080
export CORSANYWHERE_WHITELIST_TARGET=https://example.com,http://example.com,http://example.com:8080
node server.js
```

This application can immediately be run on Heroku, see https://devcenter.heroku.com/articles/nodejs
for instructions. Note that their [Acceptable Use Policy](https://www.heroku.com/policy/aup) forbids
the use of Heroku for operating an open proxy, so make sure that you either enforce a whitelist as
Expand All @@ -157,6 +169,12 @@ export CORSANYWHERE_RATELIMIT='50 3 my.example.com my2.example.com'
node server.js
```

To enable proxying to all sites except to example.com, use:
```
export PORT=8080
export CORSANYWHERE_BLACKLIST_TARGET=https://example.com,http://example.com
node server.js
```

## License

Expand Down
15 changes: 15 additions & 0 deletions lib/cors-anywhere.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ function getHandler(options, proxy) {
maxRedirects: 5, // Maximum number of redirects to be followed.
originBlacklist: [], // Requests from these origins will be blocked.
originWhitelist: [], // If non-empty, requests not from an origin in this list will be blocked.
targetBlacklist: [], // Requests to these targets will be blocked.
targetWhitelist: [], // If non-empty, requests not to an target in this list will be blocked.
checkRateLimit: null, // Function that may enforce a rate-limit by returning a non-empty string.
redirectSameOrigin: false, // Redirect the client to the requested URL for same-origin requests.
requireHeader: null, // Require a header to be set?
Expand Down Expand Up @@ -342,6 +344,19 @@ function getHandler(options, proxy) {
return;
}

var target = location.protocol + '//' + location.hostname;
if (corsAnywhere.targetBlacklist.indexOf(target) >= 0) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The target "' + target + '" was blacklisted by the operator of this proxy.');
return;
}

if (corsAnywhere.targetWhitelist.length && corsAnywhere.targetWhitelist.indexOf(target) === -1) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The target "' + target + '" was not whitelisted by the operator of this proxy.');
return;
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check can be bypassed if the whitelisted target site has an open redirect. A better place to enforce this is in the proxyRequest function.

Locally I started with something like this (many months ago), but I never got to finish the implementation.

 function proxyRequest(req, res, proxy) {
   var location = req.corsAnywhereRequestState.location;
+  // TODO: Add something like this?
+  // For https://github.com/Rob--W/cors-anywhere/issues/67
+  if (req.corsAnywhereRequestState.checkRequestAllowed &&
+      !req.corsAnywhereRequestState.checkRequestAllowed(location)) {
+      res.writeHead(403, 'Forbidden', withCORS({}, req));
+      res.end('The requested resource was blocked by the operator of this proxy.');
+    return;
+  }

The idea behind this is that the validator can be as flexible as anyone wants to, with some default implementation in server.js that simply looks at the environment variables and the prefix (like you're doing right now).

Matching by prefix is the simplest, but is it sufficient for most use cases? I recall another bug that wanted to match by "file extension".

var rateLimitMessage = corsAnywhere.checkRateLimit && corsAnywhere.checkRateLimit(origin);
if (rateLimitMessage) {
res.writeHead(429, 'Too Many Requests', cors_headers);
Expand Down
6 changes: 5 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var port = process.env.PORT || 8080;
// use originWhitelist instead.
var originBlacklist = parseEnvList(process.env.CORSANYWHERE_BLACKLIST);
var originWhitelist = parseEnvList(process.env.CORSANYWHERE_WHITELIST);
var targetBlacklist = parseEnvList(process.env.CORSANYWHERE_TARGET_BLACKLIST);
var targetWhitelist = parseEnvList(process.env.CORSANYWHERE_TARGET_WHITELIST);
function parseEnvList(env) {
if (!env) {
return [];
Expand All @@ -23,6 +25,8 @@ var cors_proxy = require('./lib/cors-anywhere');
cors_proxy.createServer({
originBlacklist: originBlacklist,
originWhitelist: originWhitelist,
targetBlacklist: targetBlacklist,
targetWhitelist: targetWhitelist,
requireHeader: ['origin', 'x-requested-with'],
checkRateLimit: checkRateLimit,
removeHeaders: [
Expand All @@ -38,7 +42,7 @@ cors_proxy.createServer({
httpProxyOptions: {
// Do not add X-Forwarded-For, etc. headers, because Heroku already adds it.
xfwd: false,
},
}
}).listen(port, host, function() {
console.log('Running CORS Anywhere on ' + host + ':' + port);
});
48 changes: 48 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,54 @@ describe('originWhitelist', function() {
});
});

describe('targetBlacklist', function() {
before(function() {
cors_anywhere = createServer({
targetBlacklist: ['http://example.com'],
});
cors_anywhere_port = cors_anywhere.listen(0).address().port;
});
after(stopServer);

it('GET /http://example.com with denied target http://example.com', function(done) {
request(cors_anywhere)
.get('/http://example.com/')
.expect('Access-Control-Allow-Origin', '*')
.expect(403, done);
});

it('GET /https://example.com with denied target http://example.com', function(done) {
request(cors_anywhere)
.get('/https://example.com/') // Note: different scheme!
.expect('Access-Control-Allow-Origin', '*')
.expect(200, done);
});
});

describe('targetWhitelist', function() {
before(function() {
cors_anywhere = createServer({
targetWhitelist: ['http://example.com'],
});
cors_anywhere_port = cors_anywhere.listen(0).address().port;
});
after(stopServer);

it('GET /http://example.com with permitted target http://example.com', function(done) {
request(cors_anywhere)
.get('/http://example.com/')
.expect('Access-Control-Allow-Origin', '*')
.expect(200, done);
});

it('GET /https://example.com with permitted target http://example.com', function(done) {
request(cors_anywhere)
.get('/https://example.com') // Note: different scheme!
.expect('Access-Control-Allow-Origin', '*')
.expect(403, done);
});
});

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add tests where the request to a whitelisted target is redirected. One test for redirection to a whitelisted target, and another test for redirection to a blacklisted target.

describe('checkRateLimit', function() {
afterEach(stopServer);

Expand Down