cloudflare-cors-anywhere, read from the source
cloudflare-cors-anywhere is the most forked way to put a free CORS proxy on your own Cloudflare account: one JavaScript file, deployed with a single wrangler command, no third-party account for the proxy itself. We read that file end to end rather than going by the README, because what a script's comments promise and what it checks at runtime aren't always the same thing.
Disclosure: corsproxy.dev is our product. Everything below about cloudflare-cors-anywhere comes from its own MIT-licensed GitHub repository — specifically index.js and the README, as read on the date above.
What it is
Written by Zibri in 2019, cloudflare-cors-anywhere has drawn over 800 GitHub stars and more than 400 forks: a Cloudflare Worker you deploy to your own account with npx wrangler deploy, built around a single index.js. It answers requests shaped like https://your-worker.workers.dev/?https://target.example/path, adds the CORS headers the target didn't send, and optionally lets you attach headers the browser would otherwise refuse to send. There's a public demo at test.cors.workers.dev for testing, and the README is upfront that abusing the demo gets you banned. Running your own copy is the intended way to use it.
The origin check is wide open by default
The script's config section is two lines:
const blacklistUrls = []; // regexp for blacklisted urls
const whitelistOrigins = [ ".*" ]; // regexp for whitelisted origins
A comment right above the file notes the blacklist "is currently unused," and the whitelist is a single regex that matches anything. Deployed as-is, the Worker answers every Origin header it receives by echoing that exact value back as Access-Control-Allow-Origin, which functionally means no origin restriction at all. That's a reasonable default for a personal proxy you're calling from your own pages, and a bad one for a Worker URL that ends up reachable by anyone else.
Authorization and Cookie headers reach the target untouched
Before forwarding a request, the script filters out headers matching five patterns: anything starting with origin, anything containing eferer (Referer), anything starting with cf- or x-forw, and its own x-cors-headers control header. Authorization and Cookie don't match any of those patterns, so whatever the browser sent goes straight to the target URL — and the target URL itself is never checked against any host, IP, or address-space list. There's no SSRF filter in the code: fetch(targetUrl, newRequest) runs on whatever URL the query string contains. A Worker with that combination, reachable from any origin, will relay a caller's bearer token or session cookie to any address it's pointed at, which is the credential-laundering and SSRF pairing we cover in how a CORS proxy works, and how it can be abused.
Redirects are followed, and every response header comes back
The forwarded request sets redirect: "follow", so a 3xx from the target is chased automatically with no re-check of where it lands. On the way back, the script exposes every response header to the browser, including ones fetch normally hides, by JSON-dumping them into a cors-received-headers header and listing it in Access-Control-Expose-Headers. The README's own usage example shows this working for Set-Cookie:
// allows to read all headers (even forbidden headers like set-cookies)
const headers = JSON.parse(res.headers.get('cors-received-headers'))
That's a deliberate feature, not a bug: it's genuinely useful for debugging an API from the browser. It's worth knowing it's on by default if the proxy sits between a visitor and anything that sets session cookies.
The printed rate limit isn't enforced anywhere in the code
Hit the Worker's base URL with no query string and it returns a status page that includes this line:
Limits: 100,000 requests/day
1,000 requests/10 minutes
index.js has no counter, no KV or D1 lookup, and no logic that checks a request count against either number. Nothing in the deployed script enforces them. Those limits most likely describe Zibri's own public demo, which would be capped at the Cloudflare account or zone level rather than inside the Worker script — but if you deploy your own copy, that text is just text printed on a page; nothing stops request 100,001.
What this is fine for, and what it isn't
None of this makes the script badly built for what it's for: a quick, free, personal CORS bypass you point at APIs you trust, called from a frontend you control. That's most of what people reach for a CORS proxy to do, and npx wrangler deploy genuinely is that easy. It stops being low-risk the moment the Worker's URL is reachable by anyone else and the target isn't restricted, because at that point it's an open relay that will fetch any address and attach a caller's own Authorization or Cookie header to the request. If you'd rather see the broader tradeoffs of running your own proxy versus using a managed one, we cover that separately in self-host vs managed CORS proxy.
Where the defaults differ
| cloudflare-cors-anywhere | our open-source Go relay | corsproxy.dev | |
|---|---|---|---|
| Origin allowed by default | Any (.* whitelist) | Any (ALLOWED_ORIGINS defaults to *) | A key's one custom origin if set (Free keys also always allow localhost); any origin if none is set |
| Authorization / Cookie to the target | Forwarded, no exclusion | Forwarded; every header except Host goes through | Stripped before the upstream call unless added back with managed upstream headers or reqHeaders |
| Private/loopback/link-local targets | No check in the code | Blocked by default, fails closed, re-checked on every redirect hop | Blocked (403 BLOCKED_HOST); the DNS check is a separate pre-fetch lookup that fails open if the resolver errors |
| Rate limiting | None in the code; the printed numbers aren't enforced | Optional per-IP per-minute limit, off by default | Per-account daily quota, enforced atomically in a Durable Object |
| Redirects | Followed automatically, no re-check | Followed, up to 10, re-checked each hop | Not followed; a 3xx is returned to the caller as is |
Our Go relay shares two of cloudflare-cors-anywhere's defaults — open origins, full header forwarding — because it's built for the same quick-start, no-accounts use case. It differs where SSRF is concerned: private and loopback targets are blocked out of the box, and that check fails closed instead of open.
If you're already running it
Four changes close most of the gap, in rough order of impact: add a target-host allowlist before the fetch call so the Worker can't be pointed at an arbitrary address; strip Authorization and Cookie unless you specifically need to forward them; swap redirect: "follow" for "manual" if you don't want 3xx responses chased automatically; and set whitelistOrigins to your actual origin instead of .* once you know what it is. None of that requires leaving Cloudflare Workers, and it turns the same script into something closer to what its printed limits imply it already does.
Try it
If patching someone else's script isn't how you want to spend an afternoon, get a free corsproxy.dev key — the free tier already strips Authorization/Cookie and blocks private targets by default.