From ba543c1f2519e6f0cfba988aca130f1a59c2d7b4 Mon Sep 17 00:00:00 2001 From: Matteo Mekhail <67237370+matteoiscrying@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:57:13 +1100 Subject: [PATCH 1/2] This should fix the vuln from that script --- server.ts | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/server.ts b/server.ts index 4dc5fc6..585d2f4 100644 --- a/server.ts +++ b/server.ts @@ -74,6 +74,9 @@ const ADMIN_LIMIT_PER_MIN = parsePositiveInt( ); const MAX_WS_GLOBAL = parsePositiveInt(process.env.MAX_WS_GLOBAL, 100_000); const MAX_WS_PER_IP = parsePositiveInt(process.env.MAX_WS_PER_IP, 8); +const MAX_WS_NEW_PER_SEC = parsePositiveInt(process.env.MAX_WS_NEW_PER_SEC, 50); +let wsNewConnections = 0; +let wsNewConnectionsResetAt = Date.now() + 1000; const MAX_HISTORY_PAGE = parsePositiveInt( process.env.MAX_HISTORY_PAGE, 100_000, @@ -101,21 +104,33 @@ function parsePositiveInt(value: string | undefined, fallback: number): number { return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } -function getClientIp(req: Request, server: Bun.Server): string { - // Railway's edge proxy strips client-provided X-Real-IP and sets the actual - // client IP. All traffic goes through the edge proxy — it cannot be bypassed. - // As a fallback, use the rightmost X-Forwarded-For value (the one Railway - // appends), then Bun's requestIP (which sees the proxy IP on Railway). - const realIp = req.headers.get("x-real-ip")?.trim(); - if (realIp) return realIp; +function isPrivateIp(ip: string): boolean { + if (ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1") return true; + if (ip.startsWith("10.")) return true; + if (ip.startsWith("192.168.")) return true; + if (ip.startsWith("fc") || ip.startsWith("fd")) return true; + if (ip.startsWith("172.")) { + const second = parseInt(ip.split(".")[1], 10); + if (second >= 16 && second <= 31) return true; + } + return false; +} - const xff = req.headers.get("x-forwarded-for"); - if (xff) { - const rightmost = xff.split(",").at(-1)?.trim(); - if (rightmost) return rightmost; +function getClientIp(req: Request, server: Bun.Server): string { + const socketIp = server.requestIP(req)?.address ?? "unknown"; + + // Only trust proxy headers when the direct connection comes from + // a private IP (i.e. Railway's edge proxy). Direct public connections + // cannot spoof their IP this way. + if (socketIp !== "unknown" && isPrivateIp(socketIp)) { + const xff = req.headers.get("x-forwarded-for"); + if (xff) { + const rightmost = xff.split(",").at(-1)?.trim(); + if (rightmost) return rightmost; + } } - return server.requestIP(req)?.address ?? "unknown"; + return socketIp; } function isRateLimited(key: string, limit: number, windowMs: number): boolean { @@ -561,6 +576,14 @@ const server = Bun.serve({ headers: { Allow: "GET" }, }); } + const now = Date.now(); + if (now >= wsNewConnectionsResetAt) { + wsNewConnections = 0; + wsNewConnectionsResetAt = now + 1000; + } + if (++wsNewConnections > MAX_WS_NEW_PER_SEC) { + return new Response("Too Many Requests", { status: 429 }); + } if (clients.size >= MAX_WS_GLOBAL) { log("WARN", "ws", "Global WS limit reached, rejecting", { ip, From 0dcb6f71ab15da16a3897afe9834ad3f610d2b51 Mon Sep 17 00:00:00 2001 From: Matteo Mekhail <67237370+matteoiscrying@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:04:06 +1100 Subject: [PATCH 2/2] Suggestion from coderabbit and macros, both valid --- server.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server.ts b/server.ts index 585d2f4..ada80a1 100644 --- a/server.ts +++ b/server.ts @@ -105,12 +105,13 @@ function parsePositiveInt(value: string | undefined, fallback: number): number { } function isPrivateIp(ip: string): boolean { - if (ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1") return true; - if (ip.startsWith("10.")) return true; - if (ip.startsWith("192.168.")) return true; + const v4 = ip.startsWith("::ffff:") ? ip.slice(7) : ip; + if (v4 === "127.0.0.1" || ip === "::1") return true; + if (v4.startsWith("10.")) return true; + if (v4.startsWith("192.168.")) return true; if (ip.startsWith("fc") || ip.startsWith("fd")) return true; - if (ip.startsWith("172.")) { - const second = parseInt(ip.split(".")[1], 10); + if (v4.startsWith("172.")) { + const second = parseInt(v4.split(".")[1] ?? "", 10); if (second >= 16 && second <= 31) return true; } return false; @@ -581,7 +582,7 @@ const server = Bun.serve({ wsNewConnections = 0; wsNewConnectionsResetAt = now + 1000; } - if (++wsNewConnections > MAX_WS_NEW_PER_SEC) { + if (wsNewConnections >= MAX_WS_NEW_PER_SEC) { return new Response("Too Many Requests", { status: 429 }); } if (clients.size >= MAX_WS_GLOBAL) { @@ -607,6 +608,7 @@ const server = Bun.serve({ log("WARN", "ws", "WebSocket upgrade failed", { ip }); return new Response("WebSocket upgrade failed", { status: 400 }); } + wsNewConnections++; return undefined; }