Skip to content

sec(server): block direct IP requests bypassing Cloudflare proxy in p…#4023

Open
berkelmali wants to merge 2 commits into
openfrontio:mainfrom
berkelmali:sec/direct-ip-access-protection
Open

sec(server): block direct IP requests bypassing Cloudflare proxy in p…#4023
berkelmali wants to merge 2 commits into
openfrontio:mainfrom
berkelmali:sec/direct-ip-access-protection

Conversation

@berkelmali
Copy link
Copy Markdown
Contributor

Description:

Prevent attackers from bypassing Cloudflare Turnstile and global rate limiting by accessing worker ports directly via server IP addresses.

Previously, individual game cluster workers hosted public HTTP/WebSocket listeners directly on public ports 3001, 3002, etc. If the server firewall is exposed or misconfigured, attackers could query the worker nodes directly on these ports (e.g. 123.45.67.89:3001), completely bypassing Cloudflare Turnstile (captchas) and Master node rate-limiting configurations.

This fix secures the application layer against proxy bypasses:

  1. Added HTTP request middleware in both Master.ts and Worker.ts for production and preproduction environments. It validates that the Host header is a domain (not a raw IP) and that the incoming request contains Cloudflare proxy headers (e.g. cf-connecting-ip).
  2. Added the same request-validation logic to the WebSocket connection handler upgrade flow in Worker.ts, instantly terminating any socket handshakes that did not route through the Cloudflare proxy.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

barfires

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Review Change Stack

Walkthrough

Adds Cloudflare/loopback IP helpers and environment-gated middleware/upgrade guards that block HTTP requests (403) and close WebSocket connections (1002) appearing to bypass Cloudflare in Prod/Preprod. Changes span Master.ts, Worker.ts, WorkerLobbyService.ts, a new Cloudflare.ts, and unit tests.

Changes

Cloudflare Proxy Bypass Protection

Layer / File(s) Summary
Cloudflare IP helpers
src/server/Cloudflare.ts
Exports Cloudflare IPv4/IPv6 CIDR lists and implements IPv4/IPv6 CIDR membership, IPv4-mapped-IPv6 normalization, isCloudflareIp, isLoopbackIp, and isCloudflareOrLoopbackIp.
HTTP middleware in Master and Worker
src/server/Master.ts, src/server/Worker.ts
Adds Prod/Preprod Express middleware that inspects req.socket.remoteAddress and Host; returns 403 when remoteAddress is not Cloudflare/loopback or when Host is IP-like and remoteAddress is not loopback.
WebSocket upgrade/connection guards
src/server/Worker.ts, src/server/WorkerLobbyService.ts
Adds Prod/Preprod WebSocket guards: close connections with code 1002 and reason "Forbidden" or reject upgrade (403) when cf-connecting-ip is missing or Host looks like an IP and remote address is non-local.
Unit tests
tests/server/Cloudflare.test.ts
Vitest tests for loopback, Cloudflare CIDR checks, and the combined predicate with positive and negative assertions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • evanpelle

Poem

A sentinel checks each line and host,
CIDRs counted, edge cases diagnosed,
In Prod and Preprod the gate now stays,
Cloudflare shields the traffic's ways,
Forbidden closes the sneak and boast.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main security change: blocking direct IP requests that bypass the Cloudflare proxy in production/preproduction environments.
Description check ✅ Passed The description is directly related to the changeset, explaining the security problem being solved and how the fix addresses it through middleware and WebSocket validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/Master.ts`:
- Around line 61-70: The middleware added in Master.ts (the app.use(...) that
checks req.headers["cf-connecting-ip"]) is using a spoofable header for
security; replace this trust-with-header logic by validating the TCP source
(req.socket.remoteAddress) against an authoritative Cloudflare IP set or,
preferably, move the allow/block decision to your network layer (firewall/load
balancer) or enable Cloudflare Authenticated Origin Pulls/Tunnel so only
Cloudflare can reach the origin; keep cf-connecting-ip only for best-effort
logging, and update the middleware in Master.ts to perform a remote-address vs.
Cloudflare CIDR check (or short-circuit to rely on the infrastructure-level
check) rather than gating on the presence of cf-connecting-ip.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5613f17-9e04-4710-9c4a-19ef6b7662bb

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6342c and d9d75f8.

📒 Files selected for processing (2)
  • src/server/Master.ts
  • src/server/Worker.ts

Comment thread src/server/Master.ts
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 26, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
src/server/Master.ts (1)

63-86: ⚡ Quick win

Use isLoopbackIp() for the secondary loopback check.

Lines 77-78 use substring matching (clientIp.includes("127.0.0.1")) while line 66 uses isCloudflareOrLoopbackIp(). For consistency and correctness, reuse the helper function.

♻️ Proposed fix
+import { isCloudflareOrLoopbackIp, isLoopbackIp } from "./Cloudflare";
-import { isCloudflareOrLoopbackIp } from "./Cloudflare";
     const host = req.headers.host ?? "";
     const isIpHost = /^[0-9.:]+$/.test(host);
-    if (
-      isIpHost &&
-      !clientIp.includes("127.0.0.1") &&
-      !clientIp.includes("::1")
-    ) {
+    if (isIpHost && !isLoopbackIp(clientIp)) {
       log.warn(`Bypassed Cloudflare proxy. Host header was an IP: ${host}`);
       return res.status(403).send("Forbidden: Direct IP access is blocked.");
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/Master.ts` around lines 63 - 86, In the middleware in Master.ts
that blocks non-Cloudflare direct connections, replace the ad-hoc substring
checks on clientIp (the two clientIp.includes("127.0.0.1") /
clientIp.includes("::1") conditions) with the helper isLoopbackIp(clientIp) and
use it consistently with isCloudflareOrLoopbackIp(); specifically, in the
host-is-IP branch inside the app.use(...) callback, change the condition to
check !isLoopbackIp(clientIp) (or combine appropriately with
isCloudflareOrLoopbackIp) so loopback detection is centralized via the
isLoopbackIp() helper.
src/server/Worker.ts (1)

108-131: ⚡ Quick win

Use isLoopbackIp() for the secondary loopback check.

Same pattern issue as in Master.ts. Lines 122-123 use substring matching while line 111 uses isCloudflareOrLoopbackIp().

♻️ Proposed fix
+import { isCloudflareOrLoopbackIp, isLoopbackIp } from "./Cloudflare";
-import { isCloudflareOrLoopbackIp } from "./Cloudflare";
       const host = req.headers.host ?? "";
       const isIpHost = /^[0-9.:]+$/.test(host);
-      if (
-        isIpHost &&
-        !clientIp.includes("127.0.0.1") &&
-        !clientIp.includes("::1")
-      ) {
+      if (isIpHost && !isLoopbackIp(clientIp)) {
         log.warn(`Bypassed Cloudflare proxy. Host header was an IP: ${host}`);
         return res.status(403).send("Forbidden: Direct IP access is blocked.");
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/Worker.ts` around lines 108 - 131, The middleware in the app.use
block uses substring checks clientIp.includes("127.0.0.1") and
clientIp.includes("::1") to detect loopback addresses; change this to call the
shared helper isLoopbackIp(clientIp) (same approach used by
isCloudflareOrLoopbackIp) so the check becomes !isLoopbackIp(clientIp) when
deciding to block IP-host requests; update the conditional in the middleware
that currently references clientIp.includes(...) to use isLoopbackIp and ensure
the import/visibility of isLoopbackIp is present.
src/server/WorkerLobbyService.ts (1)

106-134: ⚡ Quick win

Use isLoopbackIp() for consistency; remove unnecessary type cast.

Same loopback check pattern issue. Also, socket.remoteAddress exists on net.Socket, so the as any cast is not needed.

♻️ Proposed fix
+import { isCloudflareOrLoopbackIp, isLoopbackIp } from "./Cloudflare";
-import { isCloudflareOrLoopbackIp } from "./Cloudflare";
       if (
         ServerEnv.env() === GameEnv.Prod ||
         ServerEnv.env() === GameEnv.Preprod
       ) {
-        const clientIp = (socket as any).remoteAddress ?? "";
+        const clientIp = socket.remoteAddress ?? "";
         if (!isCloudflareOrLoopbackIp(clientIp)) {
           this.log.warn(
             `WebSocket upgrade rejected: Bypassed Cloudflare proxy. Remote IP: ${clientIp}`,
           );
           socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
           socket.destroy();
           return;
         }

         const host = request.headers.host ?? "";
         const isIpHost = /^[0-9.:]+$/.test(host);
-        if (
-          isIpHost &&
-          !clientIp.includes("127.0.0.1") &&
-          !clientIp.includes("::1")
-        ) {
+        if (isIpHost && !isLoopbackIp(clientIp)) {
           this.log.warn(
             `WebSocket upgrade rejected: Bypassed Cloudflare proxy. Host header was an IP: ${host}`,
           );
           socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
           socket.destroy();
           return;
         }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/WorkerLobbyService.ts` around lines 106 - 134, Replace the ad-hoc
loopback string checks and the unnecessary type cast: read the client IP
directly from socket.remoteAddress (remove "(socket as any)" cast) and instead
of testing clientIp.includes("127.0.0.1")/("::1") use the existing utility
isLoopbackIp(clientIp). Keep the prior isCloudflareOrLoopbackIp(clientIp) check
and the host-is-IP gate, but change the second condition to use
!isLoopbackIp(clientIp) to ensure consistent loopback detection via the shared
helper (referencing socket.remoteAddress, isCloudflareOrLoopbackIp,
isLoopbackIp, and the host header check).
src/server/Cloudflare.ts (1)

3-29: Cloudflare CIDR lists match the official published ranges

The hardcoded IPv4/IPv6 CIDR lists in src/server/Cloudflare.ts match the current values from Cloudflare’s published endpoints (/ips-v4 and /ips-v6), so there’s no immediate staleness issue.

Optional: add a short comment with the source URLs and the last-checked date (or a lightweight refresh strategy) to prevent future drift when Cloudflare changes them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/Cloudflare.ts` around lines 3 - 29, The CIDR arrays
CLOUDFLARE_IPV4_CIDRS and CLOUDFLARE_IPV6_CIDRS are hardcoded without
provenance; add a short comment above these exports that lists the Cloudflare
source URLs (/ips-v4 and /ips-v6), the date you last verified them, and a
suggested lightweight refresh strategy (e.g., note to periodically compare
against those endpoints or fetch during startup if needed) so future maintainers
know where and when to update them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/server/Cloudflare.ts`:
- Around line 3-29: The CIDR arrays CLOUDFLARE_IPV4_CIDRS and
CLOUDFLARE_IPV6_CIDRS are hardcoded without provenance; add a short comment
above these exports that lists the Cloudflare source URLs (/ips-v4 and /ips-v6),
the date you last verified them, and a suggested lightweight refresh strategy
(e.g., note to periodically compare against those endpoints or fetch during
startup if needed) so future maintainers know where and when to update them.

In `@src/server/Master.ts`:
- Around line 63-86: In the middleware in Master.ts that blocks non-Cloudflare
direct connections, replace the ad-hoc substring checks on clientIp (the two
clientIp.includes("127.0.0.1") / clientIp.includes("::1") conditions) with the
helper isLoopbackIp(clientIp) and use it consistently with
isCloudflareOrLoopbackIp(); specifically, in the host-is-IP branch inside the
app.use(...) callback, change the condition to check !isLoopbackIp(clientIp) (or
combine appropriately with isCloudflareOrLoopbackIp) so loopback detection is
centralized via the isLoopbackIp() helper.

In `@src/server/Worker.ts`:
- Around line 108-131: The middleware in the app.use block uses substring checks
clientIp.includes("127.0.0.1") and clientIp.includes("::1") to detect loopback
addresses; change this to call the shared helper isLoopbackIp(clientIp) (same
approach used by isCloudflareOrLoopbackIp) so the check becomes
!isLoopbackIp(clientIp) when deciding to block IP-host requests; update the
conditional in the middleware that currently references clientIp.includes(...)
to use isLoopbackIp and ensure the import/visibility of isLoopbackIp is present.

In `@src/server/WorkerLobbyService.ts`:
- Around line 106-134: Replace the ad-hoc loopback string checks and the
unnecessary type cast: read the client IP directly from socket.remoteAddress
(remove "(socket as any)" cast) and instead of testing
clientIp.includes("127.0.0.1")/("::1") use the existing utility
isLoopbackIp(clientIp). Keep the prior isCloudflareOrLoopbackIp(clientIp) check
and the host-is-IP gate, but change the second condition to use
!isLoopbackIp(clientIp) to ensure consistent loopback detection via the shared
helper (referencing socket.remoteAddress, isCloudflareOrLoopbackIp,
isLoopbackIp, and the host header check).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdd76940-2170-4ff2-b477-860319a635e6

📥 Commits

Reviewing files that changed from the base of the PR and between d9d75f8 and 163a935.

📒 Files selected for processing (5)
  • src/server/Cloudflare.ts
  • src/server/Master.ts
  • src/server/Worker.ts
  • src/server/WorkerLobbyService.ts
  • tests/server/Cloudflare.test.ts

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

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant