security sweep

This commit is contained in:
2026-05-29 09:03:47 -04:00
parent 469c28fa64
commit 3b29de3234
60 changed files with 7148 additions and 413 deletions

View File

@@ -1,7 +1,59 @@
import { WebSocketServer, WebSocket } from "ws";
import type { Server } from "ws";
import type { IncomingMessage } from "node:http";
import { verifyJWT } from "~/server/auth/jwt";
/**
* Builds the trusted WebSocket origins allowlist.
* Includes localhost dev origins and APP_URL if valid.
*/
function getTrustedOrigins(): string[] {
const origins = [
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
];
// Validate APP_URL before trusting it as a WebSocket origin
const appUrl = process.env.APP_URL;
if (appUrl) {
try {
const parsed = new URL(appUrl);
if (/^https?:$/.test(parsed.protocol) && parsed.hostname) {
origins.push(appUrl);
}
} catch {
// Invalid URL — skip
}
}
// Allow explicit override via VALID_WEBSOCKET_ORIGINS (comma-separated)
const explicit = process.env.VALID_WEBSOCKET_ORIGINS;
if (explicit) {
for (const origin of explicit.split(",").map((o) => o.trim())) {
if (origin) origins.push(origin);
}
}
return origins;
}
/**
* Validates the Origin header against the trusted origins allowlist.
* Rejects missing, empty, or untrusted origins.
*/
function isTrustedOrigin(
origin: string | undefined,
trustedOrigins: string[],
): boolean {
if (!origin || !origin.trim()) return false;
return trustedOrigins.includes(origin);
}
// Pre-compute trusted origins at startup
const TRUSTED_ORIGINS = getTrustedOrigins();
const WS_PORT = parseInt(process.env.WS_PORT ?? "3001", 10);
const HEARTBEAT_INTERVAL = 30_000;
const PONG_TIMEOUT = 10_000;
@@ -146,10 +198,25 @@ export function start(): Promise<void> {
return;
}
wss = new WebSocketServer({ port: WS_PORT }, () => {
console.log(`[websocket] Server listening on port ${WS_PORT}`);
resolve();
});
wss = new WebSocketServer(
{
port: WS_PORT,
verifyClient: (info: { origin: string; req: IncomingMessage }) => {
const origin = info.req.headers.origin ?? info.origin;
if (!isTrustedOrigin(origin, TRUSTED_ORIGINS)) {
console.warn(
`[websocket] Rejected untrusted origin: ${origin ?? "(none)"}`,
);
return false;
}
return true;
},
},
() => {
console.log(`[websocket] Server listening on port ${WS_PORT}`);
resolve();
},
);
wss.on("connection", async (ws: WsClient) => {
// Mark as unauthenticated initially; client must authenticate within timeout