From 6a058930aa53c83c6daa7eda2b4be464089d49de Mon Sep 17 00:00:00 2001 From: KingKDot Date: Wed, 8 Apr 2026 21:24:23 -0500 Subject: [PATCH] 1.) Don't allow agent auth to be disabled if running in prod 2.) fix ssrf in the fetch url 3.) fix path traversal vuln when uploading files 4.) added rate limiting to web socket upgrading (very needed) 5.) make token revocation from 5 seconds to 500 ms. --- Overlord-Server/src/auth.ts | 2 +- Overlord-Server/src/server/agent-auth.ts | 9 +++-- .../src/server/routes/deploy-routes.ts | 14 ++++++++ .../src/server/routes/file-download-routes.ts | 4 ++- .../src/server/routes/ws-upgrade-routes.ts | 34 +++++++++++++++++++ 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Overlord-Server/src/auth.ts b/Overlord-Server/src/auth.ts index 69f30e7..53e7289 100644 --- a/Overlord-Server/src/auth.ts +++ b/Overlord-Server/src/auth.ts @@ -20,7 +20,7 @@ const tokenCache = new Map< string, { payload: JWTPayload; timestamp: number } >(); -const TOKEN_CACHE_TTL = 5000; +const TOKEN_CACHE_TTL = 500; function getSecretKey(): Uint8Array { const config = getConfig(); diff --git a/Overlord-Server/src/server/agent-auth.ts b/Overlord-Server/src/server/agent-auth.ts index 3f9a639..de07418 100644 --- a/Overlord-Server/src/server/agent-auth.ts +++ b/Overlord-Server/src/server/agent-auth.ts @@ -17,8 +17,13 @@ export function isAuthorizedAgentRequest( String(process.env.OVERLORD_DISABLE_AGENT_AUTH || "").toLowerCase() === "true"; if (disableAuth) { - logger.info("[auth] Agent auth explicitly disabled by OVERLORD_DISABLE_AGENT_AUTH=true"); - return true; + const nodeEnv = String(process.env.NODE_ENV || "development").toLowerCase(); + if (nodeEnv === "production") { + logger.warn("[auth] OVERLORD_DISABLE_AGENT_AUTH is ignored in production mode"); + } else { + logger.info("[auth] Agent auth explicitly disabled by OVERLORD_DISABLE_AGENT_AUTH=true (non-production mode)"); + return true; + } } const token = agentToken?.trim(); diff --git a/Overlord-Server/src/server/routes/deploy-routes.ts b/Overlord-Server/src/server/routes/deploy-routes.ts index 118119c..c9189be 100644 --- a/Overlord-Server/src/server/routes/deploy-routes.ts +++ b/Overlord-Server/src/server/routes/deploy-routes.ts @@ -113,6 +113,20 @@ export async function handleDeployRoutes( return new Response("Only http and https URLs are allowed", { status: 400 }); } + const hostname = parsed.hostname.toLowerCase(); + const BLOCKED_HOSTS = ["localhost", "metadata.google.internal", "169.254.169.254"]; + if ( + BLOCKED_HOSTS.includes(hostname) || + hostname.endsWith(".internal") || + hostname.startsWith("127.") || + hostname === "[::1]" || + /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(hostname) || + hostname.startsWith("169.254.") || + hostname.startsWith("0.") + ) { + return new Response("URLs pointing to private/internal addresses are not allowed", { status: 400 }); + } + const rawFilename = path.basename(parsed.pathname) || "download.bin"; const filename = rawFilename.replace(/[^a-zA-Z0-9._\-]/g, "_").substring(0, 128) || "download.bin"; diff --git a/Overlord-Server/src/server/routes/file-download-routes.ts b/Overlord-Server/src/server/routes/file-download-routes.ts index 3728d54..25ca67f 100644 --- a/Overlord-Server/src/server/routes/file-download-routes.ts +++ b/Overlord-Server/src/server/routes/file-download-routes.ts @@ -140,7 +140,9 @@ const FILE_UPLOAD_PULL_TTL_MS = parsePositiveIntEnv( function isSafeRemotePath(value: string): boolean { if (!value || value.length > 4096) return false; - return !/[\x00-\x1F\x7F]/.test(value); + if (/[\x00-\x1F\x7F]/.test(value)) return false; + if (/(\.\.([\/\\]|$))/.test(value)) return false; + return true; } async function serveDownloadById( diff --git a/Overlord-Server/src/server/routes/ws-upgrade-routes.ts b/Overlord-Server/src/server/routes/ws-upgrade-routes.ts index e8bbc5c..bdb3b14 100644 --- a/Overlord-Server/src/server/routes/ws-upgrade-routes.ts +++ b/Overlord-Server/src/server/routes/ws-upgrade-routes.ts @@ -11,12 +11,46 @@ type WsUpgradeDeps = { isAuthorizedAgentRequest: (req: Request, url: URL) => boolean; }; +const WS_RATE_WINDOW_MS = 10_000; +const WS_RATE_MAX = 30; +const wsRateMap = new Map(); + +setInterval(() => { + const now = Date.now(); + for (const [ip, entry] of wsRateMap.entries()) { + if (now - entry.windowStart > WS_RATE_WINDOW_MS * 2) { + wsRateMap.delete(ip); + } + } +}, 30_000); + +function isWsRateLimited(ip: string): boolean { + if (!ip) return false; + const now = Date.now(); + const entry = wsRateMap.get(ip); + if (!entry || now - entry.windowStart > WS_RATE_WINDOW_MS) { + wsRateMap.set(ip, { count: 1, windowStart: now }); + return false; + } + entry.count++; + if (entry.count > WS_RATE_MAX) { + logger.warn(`[rate-limit] WebSocket upgrade rate limit exceeded for IP ${ip}`); + return true; + } + return false; +} + export async function handleWsUpgradeRoutes( req: Request, url: URL, server: RequestServer, deps: WsUpgradeDeps, ): Promise { + const ip = server.requestIP(req)?.address || ""; + if (isWsRateLimited(ip)) { + return new Response("Too Many Requests", { status: 429 }); + } + const consoleWsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/console\/ws$/); if (consoleWsMatch) { const user = await authenticateRequest(req);