From e5b5e87da2c86905f52ca26948efcd9d62ca2a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Maia?= Date: Sat, 4 Apr 2026 01:28:51 +0100 Subject: [PATCH] Fix wildcard CORS, restrict credential file permissions, and add request body size limit - Replace Access-Control-Allow-Origin: * with a localhost-only allowlist (localhost:4100, localhost:3000, and 127.0.0.1 equivalents) to prevent cross-site data exfiltration from any malicious webpage. - Write credentials.json and config.json with mode 0600 (owner-only) and create the .relayplane directory with mode 0700. Previously these files were created with default 0644 permissions, allowing any local user to read API keys. - Add a 1 MB size limit to readBody() to reject oversized payloads before they exhaust server memory. Protects simulation and policy test endpoints from denial-of-service via large JSON bodies. --- src/config.ts | 8 ++++---- src/credentials.ts | 4 ++-- src/server.ts | 24 ++++++++++++++++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/config.ts b/src/config.ts index 8645376..1e6f846 100644 --- a/src/config.ts +++ b/src/config.ts @@ -242,7 +242,7 @@ function generateDeviceId(): string { */ function ensureConfigDir(): void { if (!fs.existsSync(CONFIG_DIR)) { - fs.mkdirSync(CONFIG_DIR, { recursive: true }); + fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); } } @@ -384,9 +384,9 @@ export function saveConfig(config: ProxyConfig): void { } } - // Atomic write: write to tmp, then rename + // Atomic write: write to tmp with restricted permissions, then rename const data = JSON.stringify(config, null, 2); - fs.writeFileSync(CONFIG_TMP, data); + fs.writeFileSync(CONFIG_TMP, data, { mode: 0o600 }); fs.renameSync(CONFIG_TMP, CONFIG_FILE); } @@ -459,7 +459,7 @@ export function setApiKey(key: string): void { creds = JSON.parse(fs.readFileSync(credPath, 'utf-8')); } creds.apiKey = key; - fs.writeFileSync(credPath, JSON.stringify(creds, null, 2)); + fs.writeFileSync(credPath, JSON.stringify(creds, null, 2), { mode: 0o600 }); } catch {} } diff --git a/src/credentials.ts b/src/credentials.ts index 211567a..f691156 100644 --- a/src/credentials.ts +++ b/src/credentials.ts @@ -77,12 +77,12 @@ export function saveAgentCredentials(creds: Partial): void { const dir = path.dirname(credPath); if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } const existing = loadAgentCredentials(); const merged: AgentCredentials = { ...existing, ...creds }; - fs.writeFileSync(credPath, JSON.stringify(merged, null, 2) + '\n'); + fs.writeFileSync(credPath, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 }); } /** diff --git a/src/server.ts b/src/server.ts index 76b4cbe..320c36d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -404,8 +404,12 @@ export class ProxyServer { private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { const url = new URL(req.url ?? '/', `http://${req.headers.host}`); - // CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); + // CORS headers — restrict to localhost origins to prevent cross-site data exfiltration + const origin = req.headers.origin as string | undefined; + const allowedOrigins = ['http://localhost:4100', 'http://localhost:3000', 'http://127.0.0.1:4100', 'http://127.0.0.1:3000']; + if (origin && allowedOrigins.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-RelayPlane-Workspace, X-RelayPlane-Agent, X-RelayPlane-Session, X-RelayPlane-Automated'); @@ -1370,13 +1374,25 @@ export class ProxyServer { ); } + /** Maximum request body size (1 MB) */ + private static readonly MAX_BODY_SIZE = 1_048_576; + /** - * Read request body + * Read request body with size limit to prevent memory exhaustion from oversized payloads. */ private readBody(req: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { let body = ''; - req.on('data', (chunk) => (body += chunk)); + let size = 0; + req.on('data', (chunk: Buffer | string) => { + size += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length; + if (size > ProxyServer.MAX_BODY_SIZE) { + req.destroy(); + reject(new Error('Request body exceeds 1 MB size limit')); + return; + } + body += chunk; + }); req.on('end', () => resolve(body)); req.on('error', reject); });