This should fix the vuln from that script
This commit is contained in:
47
server.ts
47
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_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_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(
|
const MAX_HISTORY_PAGE = parsePositiveInt(
|
||||||
process.env.MAX_HISTORY_PAGE,
|
process.env.MAX_HISTORY_PAGE,
|
||||||
100_000,
|
100_000,
|
||||||
@@ -101,21 +104,33 @@ function parsePositiveInt(value: string | undefined, fallback: number): number {
|
|||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientIp(req: Request, server: Bun.Server<WsData>): string {
|
function isPrivateIp(ip: string): boolean {
|
||||||
// Railway's edge proxy strips client-provided X-Real-IP and sets the actual
|
if (ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1") return true;
|
||||||
// client IP. All traffic goes through the edge proxy — it cannot be bypassed.
|
if (ip.startsWith("10.")) return true;
|
||||||
// As a fallback, use the rightmost X-Forwarded-For value (the one Railway
|
if (ip.startsWith("192.168.")) return true;
|
||||||
// appends), then Bun's requestIP (which sees the proxy IP on Railway).
|
if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
|
||||||
const realIp = req.headers.get("x-real-ip")?.trim();
|
if (ip.startsWith("172.")) {
|
||||||
if (realIp) return realIp;
|
const second = parseInt(ip.split(".")[1], 10);
|
||||||
|
if (second >= 16 && second <= 31) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const xff = req.headers.get("x-forwarded-for");
|
function getClientIp(req: Request, server: Bun.Server<WsData>): string {
|
||||||
if (xff) {
|
const socketIp = server.requestIP(req)?.address ?? "unknown";
|
||||||
const rightmost = xff.split(",").at(-1)?.trim();
|
|
||||||
if (rightmost) return rightmost;
|
// 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 {
|
function isRateLimited(key: string, limit: number, windowMs: number): boolean {
|
||||||
@@ -561,6 +576,14 @@ const server = Bun.serve<WsData>({
|
|||||||
headers: { Allow: "GET" },
|
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) {
|
if (clients.size >= MAX_WS_GLOBAL) {
|
||||||
log("WARN", "ws", "Global WS limit reached, rejecting", {
|
log("WARN", "ws", "Global WS limit reached, rejecting", {
|
||||||
ip,
|
ip,
|
||||||
|
|||||||
Reference in New Issue
Block a user