Comparison

CORSflare, read from the source

CORSflare is a single-file Cloudflare Worker that puts a CORS-friendly reverse proxy in front of a website, popular enough (165 GitHub stars, 50 forks at the time of writing) that it comes up next to cloudflare-cors-anywhere in searches for a free Workers-based CORS proxy. We read CORSflare.js end to end instead of going by its README, because two of its defaults contradict each other in ways that only surface once a browser actually enforces the rule.

· ~7 min read

Disclosure: corsproxy.dev is our product. Everything below about CORSflare comes from its own MIT-licensed GitHub repository — specifically CORSflare.js (v1.0.5) and its README, as read on the date above.

What it is

CORSflare's README describes it as "a lightweight JavaScript CORS Reverse Proxy designed to run in a Cloudflare Worker," built for two cases: putting another site inside an <iframe>, and calling an external API from the browser. The README credits two earlier projects it's "strongly based upon": Berkeley-Reject's workers-proxy and Zibri's cloudflare-cors-anywhere, which we read from the source in an earlier post. The difference shows up immediately in the config block. Where cloudflare-cors-anywhere reads its target from the request's query string, CORSflare points at one fixed host by default:

const upstream = 'www.google.com';
...
const upstream_allow_override = false;

Out of the box, this is a single-target reverse proxy, not a general "fetch any URL" proxy. The maintainer's own comment on upstream_allow_override says what changes if you turn it on:

// WARNING: this feature will allow third-parties to replace this proxy's default
// upstream with an upstream of their choice: activate it only if you know
// what you're doing (...)

Flip that flag and CORSflare becomes the "call any API" proxy most people actually want, with no allowlist, DNS check, or IP-range check on whatever hostname lands in the query string. That's the same gap we found in cloudflare-cors-anywhere's target handling, and it's the one place CORSflare's own comments warn about it directly.

Access-Control-Allow-Origin: '*' and Allow-Credentials: true, set together

The response headers CORSflare adds to every proxied response are fixed, not computed per request:

'Access-Control-Allow-Origin': '*',
...
'Access-Control-Allow-Credentials': true,

Browsers reject that combination by design: the credentials mode spec says a response can set Access-Control-Allow-Credentials: true only if Access-Control-Allow-Origin echoes back the exact requesting origin, never the literal *. A page that calls CORSflare with credentials: 'include' (to send cookies or an HTTP auth header automatically) gets both headers back from the Worker, and the browser blocks the response anyway, console error and all, because the wildcard disqualifies it. The two settings can't both do what they're each meant to do at the same time. corsproxy.dev doesn't have this problem because it never sends the credentials header at all: Access-Control-Allow-Origin is always * and Access-Control-Allow-Credentials is never set, since the service expects callers to authenticate to the upstream with a header, not a cookie.

There's no synthetic preflight response — OPTIONS goes to the upstream too

The README describes preflight handling as a feature: "the proxy will respond to the preflight request issued by the Front End App... by setting the 'CORS allowed' headers: right after that, it will forward the request to the target server, receive its response..." Reading the request handler, there's no branch for the request method before the fetch call:

let method = request.method;
let request_headers = request.headers;
let new_request_headers = new Headers(request_headers);
...
var params = {
    method: method,
    headers: new_request_headers,
    redirect: 'manual'
}
...
let original_response = await fetch(url.href, params);

Every method, including OPTIONS, is forwarded to upstream exactly like a GET or POST would be. The only method-specific check anywhere in the file looks for an Upgrade: websocket header on the response, not for an incoming preflight. So the "CORS allowed" headers the README promises are pasted onto whatever the upstream sends back for an OPTIONS /path request, which is a different thing from answering the preflight itself. For a simple request — a plain GET, or a POST with a form-encoded body — none of this matters, because the browser never sends a preflight in the first place. It matters for a POST with Content-Type: application/json or a request carrying a custom header, both of which are preflighted: the browser's OPTIONS call now depends on the configured upstream returning something CORSflare's fixed headers can be layered onto, usually Access-Control-Allow-Headers and Access-Control-Allow-Methods, neither of which CORSflare sets itself. If the upstream doesn't answer OPTIONS the way a browser expects, which is exactly the situation a CORS proxy usually exists to work around, the preflight fails and the real request never goes out. Our own proxy and our open-source Go relay both answer OPTIONS directly instead of forwarding it; see how Access-Control-Max-Age caching works for what the browser does with that response afterward.

Authorization and Cookie headers reach the upstream untouched

Before forwarding, the script copies the incoming headers wholesale and only overwrites three of them:

let new_request_headers = new Headers(request_headers);
...
new_request_headers.set('Host', upstream_domain);
new_request_headers.set('Origin', upstream_domain);
new_request_headers.set('Referer', url.protocol + '//' + url_hostname);

Authorization and Cookie don't match any of those three names, so whatever the browser attached goes straight to upstream_domain — the fixed host by default, or whatever hostname a caller supplies if upstream_allow_override is on. There's no SSRF check on that hostname either: no block list for private, loopback, or link-local addresses, and no DNS resolution check before the fetch call. With the default single-host config that's a narrow surface, since upstream_domain can only ever be www.google.com or the mobile override. It stops being narrow the moment the override flag is on.

The only access control is a region and IP blocklist, and it checks the caller, not the target

blocked_regions and blocked_ip_addresses are checked against the request's own cf-ipcountry and cf-connecting-ip headers:

if (blocked_regions.includes(region) || blocked_ip_addresses.includes(ip_address)) {
    response = new Response('Access denied', { status: 403 });
}

That's a block list for who may call the Worker, not a check on where the Worker is allowed to send requests. There's no API key, no per-caller quota, and no rate limiting anywhere in CORSflare.js. The README's mention of "100,000 requests per day" is the free tier of the Cloudflare Workers platform itself, an account-level ceiling Cloudflare enforces, not a number CORSflare counts or checks in its own code.

Where the defaults differ

  CORSflare our open-source Go relay corsproxy.dev
Origin + credentials on the response* and Allow-Credentials: true together, which browsers rejectSpecific origin reflected when ALLOWED_ORIGINS is set; credentials header added only thenAlways *; Allow-Credentials never sent
Preflight (OPTIONS)Forwarded to the upstream; no synthetic responseAnswered directly, Allow-Headers: *Answered directly, logged separately as a preflight
Authorization / Cookie to the targetForwarded, no exclusionForwarded; every header except Host goes throughStripped before the upstream call unless added back with managed upstream headers or reqHeaders
Target / upstreamOne hardcoded host by default; any host, unchecked, if upstream_allow_override is onAny target in the query string; private/loopback targets blocked, fails closedAny public target; private/loopback/metadata addresses blocked via DNS-over-HTTPS resolution
Rate limitingNone in the code; the 100k/day figure is Cloudflare's own platform limitOptional per-IP per-minute limit, off by defaultPer-account daily quota, enforced atomically in a Durable Object
HTTP methodsAny method passed through as isAny methodGET and POST only

If you're already running it

Three changes close most of the gap without leaving Cloudflare Workers. Drop the Access-Control-Allow-Credentials: true line unless you actually send cookies through the proxy, and if you do, compute Access-Control-Allow-Origin from the request's own Origin header instead of a static wildcard. Add an explicit branch for request.method === 'OPTIONS' that returns a 204 with Access-Control-Allow-Headers and Access-Control-Allow-Methods set directly, before the code ever calls fetch, so a preflighted request doesn't depend on the upstream understanding OPTIONS. And if you turn on upstream_allow_override, pair it with your own allowlist of hostnames, since the script ships with none.

Try it

If patching someone else's Worker isn't how you want to spend an afternoon, get a free corsproxy.dev key — preflight requests are answered directly, and Authorization/Cookie headers are stripped before the upstream call by default.