Skip to content

Make cors-worker runtime-portable: Node shell + connect-time SSRF guard (#124) - #125

Merged
jonocodes merged 4 commits into
mainfrom
feature/implement-124
Oct 20, 2026
Merged

jonocodes merged 4 commits into
mainfrom
feature/implement-124

Conversation

@jonocodes

Copy link
Copy Markdown
Owner

Closes #124.

The CORS proxy can now be self-hosted without a Cloudflare account, as a Node server or Docker container. When self-hosted, it also refuses hostnames that resolve to private addresses.

Changes

  • infra/cors-worker/node.js is a Node shell for the portable core, with no dependencies. It reads PROXY_TOKENS, PUBLIC, ALLOWED_ORIGINS, PORT and HOST from the environment and has no cache. Its setup page explains how to set PROXY_TOKENS in the environment.
  • guarded-fetch.js is a fetch built on node:http/https with a guarded DNS lookup. It refuses the connection if any resolved address isn't public, and checks IP literals as well. Core follows redirects itself, so every hop goes back through the guard. Bodies pass through untouched, and chunked bodies over 25 MiB are cut off.
  • ssrf-policy.js holds the pure isBlockedAddress(ip), which future shells can reuse. It covers loopback, RFC 1918, CGNAT, link-local/metadata, reserved and documentation ranges, and IPv6 ULA, link-local and multicast. It also covers IPv6 forms that embed a private IPv4 address: mapped, compatible, NAT64, 6to4 and Teredo.
  • core.js gains two injectable options, fetch and setupSecretStep. Their defaults keep the Cloudflare behaviour.
  • Tests: shell-cases.mjs holds the shared behaviour suite and runs against both shells. New DNS-rebinding tests (direct and via a redirect) use an injected resolver, so none of them depend on external DNS.
  • Self-hosting: a Dockerfile, a cors-proxy compose service, and .env added to .gitignore.
  • Docs: both READMEs, docs/ENVIRONMENT_VARIABLES.md, docs/DEVELOPMENT.md, the changelog, and an update to ADR 0003.

Verification

  • node --test infra/cors-worker/*.test.mjs infra/cors-stats/*.test.mjs passes 119/119 on Node 20 and Node 24.
  • Temporarily disabling the policy makes every guard and rebinding test fail.
  • docker compose up cors-proxy, run with a token:
    • a request without the token gets a 404;
    • example.com is proxied with CORS headers;
    • the empty-target probe gets its 400;
    • the real 127.0.0.1.nip.io is refused.
  • With no token, the container serves the setup page.
  • A wrangler dry run bundles only the worker files, with the same (no) bindings. The bundle grew from 17.31 to 17.63 KiB because of the option plumbing.

Known gaps

  • The shared suite runs on the Node shell with a stubbed fetch. The guarded client's response handling is covered by its own tests only.
  • The upstream timeout only fires on inactivity, so there is no limit on total request time.
  • No just recipe, because the repo has no Justfile.

🤖 Generated with Claude Code

jonocodes and others added 2 commits October 17, 2026 21:04
… guard (#124)

- node.js: node:http server over the portable core; config from env
  (PROXY_TOKENS, PUBLIC, ALLOWED_ORIGINS, PORT, HOST); no deps, no cache.
- guarded-fetch.js: node:http(s) fetch whose DNS lookup refuses any
  non-public resolved address before connecting; IP literals checked too.
  Core's manual redirects send every hop back through it.
- ssrf-policy.js: pure isBlockedAddress(ip) incl. CGNAT, metadata,
  IPv4-mapped/compatible, NAT64, 6to4 and Teredo forms.
- core.js: injectable `fetch` and setup-page `setupSecretStep` options;
  Cloudflare defaults unchanged.
- Tests: shared shell-cases.mjs suite runs against both shells; new
  DNS-rebinding tests (direct and via redirect) use an injected resolver.
- Dockerfile + cors-proxy compose service; .env gitignored; docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- guarded-fetch: cut off chunked bodies past 25 MiB (core can only refuse
  an advertised length); honest error for unsupported protocols.
- node.js: a request path starting with // stays a path.
- ADR 0003: record Node as the second entry and the split SSRF check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@netlify

netlify Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for savrlist ready!

Name Link
🔨 Latest commit 9245336
🔍 Latest deploy log https://app.netlify.com/projects/savrlist/deploys/6ad44caafdc53a3f971fef54
😎 Deploy Preview https://deploy-preview-125--savrlist.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make cors-worker runtime-portable: Node shell + connect-time SSRF guard

1 participant