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.
This commit is contained in:
@@ -20,7 +20,7 @@ const tokenCache = new Map<
|
|||||||
string,
|
string,
|
||||||
{ payload: JWTPayload; timestamp: number }
|
{ payload: JWTPayload; timestamp: number }
|
||||||
>();
|
>();
|
||||||
const TOKEN_CACHE_TTL = 5000;
|
const TOKEN_CACHE_TTL = 500;
|
||||||
|
|
||||||
function getSecretKey(): Uint8Array {
|
function getSecretKey(): Uint8Array {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|||||||
@@ -17,9 +17,14 @@ export function isAuthorizedAgentRequest(
|
|||||||
String(process.env.OVERLORD_DISABLE_AGENT_AUTH || "").toLowerCase() ===
|
String(process.env.OVERLORD_DISABLE_AGENT_AUTH || "").toLowerCase() ===
|
||||||
"true";
|
"true";
|
||||||
if (disableAuth) {
|
if (disableAuth) {
|
||||||
logger.info("[auth] Agent auth explicitly disabled by OVERLORD_DISABLE_AGENT_AUTH=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;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const token = agentToken?.trim();
|
const token = agentToken?.trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|||||||
@@ -113,6 +113,20 @@ export async function handleDeployRoutes(
|
|||||||
return new Response("Only http and https URLs are allowed", { status: 400 });
|
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 rawFilename = path.basename(parsed.pathname) || "download.bin";
|
||||||
const filename = rawFilename.replace(/[^a-zA-Z0-9._\-]/g, "_").substring(0, 128) || "download.bin";
|
const filename = rawFilename.replace(/[^a-zA-Z0-9._\-]/g, "_").substring(0, 128) || "download.bin";
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,9 @@ const FILE_UPLOAD_PULL_TTL_MS = parsePositiveIntEnv(
|
|||||||
|
|
||||||
function isSafeRemotePath(value: string): boolean {
|
function isSafeRemotePath(value: string): boolean {
|
||||||
if (!value || value.length > 4096) return false;
|
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(
|
async function serveDownloadById(
|
||||||
|
|||||||
@@ -11,12 +11,46 @@ type WsUpgradeDeps = {
|
|||||||
isAuthorizedAgentRequest: (req: Request, url: URL) => boolean;
|
isAuthorizedAgentRequest: (req: Request, url: URL) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WS_RATE_WINDOW_MS = 10_000;
|
||||||
|
const WS_RATE_MAX = 30;
|
||||||
|
const wsRateMap = new Map<string, { count: number; windowStart: number }>();
|
||||||
|
|
||||||
|
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(
|
export async function handleWsUpgradeRoutes(
|
||||||
req: Request,
|
req: Request,
|
||||||
url: URL,
|
url: URL,
|
||||||
server: RequestServer,
|
server: RequestServer,
|
||||||
deps: WsUpgradeDeps,
|
deps: WsUpgradeDeps,
|
||||||
): Promise<Response | null> {
|
): Promise<Response | null> {
|
||||||
|
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$/);
|
const consoleWsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/console\/ws$/);
|
||||||
if (consoleWsMatch) {
|
if (consoleWsMatch) {
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
|
|||||||
Reference in New Issue
Block a user