security sweep
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user