From 40a19d06c65e7c63402ba7484074daaaf2f4795c Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 3 Jun 2026 13:07:22 +0530 Subject: [PATCH 1/4] fix: harden CORS proxy with timeout, size limit, and encoded path check - Decode path before traversal check to catch %2e%2e encoded variants - Add AbortSignal.timeout(30s) to upstream fetch calls - Reject request bodies larger than 1MB via Content-Length check - Return 504 for upstream timeouts instead of generic 502 Closes #118 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chronicle/src/server/api/apis-proxy.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/chronicle/src/server/api/apis-proxy.ts b/packages/chronicle/src/server/api/apis-proxy.ts index eb4a3ba9..51e52836 100644 --- a/packages/chronicle/src/server/api/apis-proxy.ts +++ b/packages/chronicle/src/server/api/apis-proxy.ts @@ -2,6 +2,9 @@ import { defineHandler, HTTPError } from 'nitro'; import { loadConfig } from '@/lib/config'; import { loadApiSpecs } from '@/lib/openapi'; +const MAX_BODY_SIZE = 1_048_576; // 1 MB +const UPSTREAM_TIMEOUT_MS = 30_000; + interface ProxyRequest { specName: string; method: string; @@ -10,11 +13,28 @@ interface ProxyRequest { body?: unknown; } +function isPathSafe(p: string): boolean { + let decoded: string; + try { + decoded = decodeURIComponent(p); + } catch { + return false; + } + if (/^[a-z]+:\/\//i.test(decoded)) return false; + const normalized = new URL(decoded, 'http://localhost').pathname; + return !normalized.split('/').includes('..'); +} + export default defineHandler(async event => { if (event.req.method !== 'POST') { throw new HTTPError({ status: 405, message: 'Method not allowed' }); } + const contentLength = parseInt(event.req.headers.get('content-length') ?? '0', 10); + if (contentLength > MAX_BODY_SIZE) { + throw new HTTPError({ status: 413, message: `Request body too large (max ${MAX_BODY_SIZE} bytes)` }); + } + const { specName, method, path, headers, body } = (await event.req.json()) as ProxyRequest; @@ -33,7 +53,7 @@ export default defineHandler(async event => { throw new HTTPError({ status: 404, message: `Unknown spec: ${specName}` }); } - if (/^[a-z]+:\/\//i.test(path) || path.includes('..')) { + if (!isPathSafe(path)) { throw new HTTPError({ status: 400, message: 'Invalid path' }); } @@ -43,7 +63,8 @@ export default defineHandler(async event => { const response = await fetch(url, { method, headers, - body: body ? JSON.stringify(body) : undefined + body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), }); const contentType = response.headers.get('content-type') ?? ''; @@ -64,6 +85,9 @@ export default defineHandler(async event => { headers: responseHeaders }); } catch (error) { + if (error instanceof DOMException && error.name === 'TimeoutError') { + throw new HTTPError({ status: 504, message: `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS}ms` }); + } const message = error instanceof Error ? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}` From 71cb3b8aaae3f81c2abbfc43a664f78e4ba20c2a Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 3 Jun 2026 13:28:23 +0530 Subject: [PATCH 2/4] fix: increase proxy body size limit to 10MB Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/apis-proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chronicle/src/server/api/apis-proxy.ts b/packages/chronicle/src/server/api/apis-proxy.ts index 51e52836..28f9035f 100644 --- a/packages/chronicle/src/server/api/apis-proxy.ts +++ b/packages/chronicle/src/server/api/apis-proxy.ts @@ -2,7 +2,7 @@ import { defineHandler, HTTPError } from 'nitro'; import { loadConfig } from '@/lib/config'; import { loadApiSpecs } from '@/lib/openapi'; -const MAX_BODY_SIZE = 1_048_576; // 1 MB +const MAX_BODY_SIZE = 10_485_760; // 10 MB const UPSTREAM_TIMEOUT_MS = 30_000; interface ProxyRequest { From 32ff460a6414c169afc2a7710e2d5e1399f22b35 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 3 Jun 2026 13:30:01 +0530 Subject: [PATCH 3/4] fix: increase proxy upstream timeout to 2 minutes Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/apis-proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chronicle/src/server/api/apis-proxy.ts b/packages/chronicle/src/server/api/apis-proxy.ts index 28f9035f..f702db45 100644 --- a/packages/chronicle/src/server/api/apis-proxy.ts +++ b/packages/chronicle/src/server/api/apis-proxy.ts @@ -3,7 +3,7 @@ import { loadConfig } from '@/lib/config'; import { loadApiSpecs } from '@/lib/openapi'; const MAX_BODY_SIZE = 10_485_760; // 10 MB -const UPSTREAM_TIMEOUT_MS = 30_000; +const UPSTREAM_TIMEOUT_MS = 120_000; interface ProxyRequest { specName: string; From e1a8382c43203787187218d3e85e8a4164d9186d Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 3 Jun 2026 13:34:21 +0530 Subject: [PATCH 4/4] refactor: use StatusCodes from http-status-codes package Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/apis-proxy.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/chronicle/src/server/api/apis-proxy.ts b/packages/chronicle/src/server/api/apis-proxy.ts index f702db45..79b2c49c 100644 --- a/packages/chronicle/src/server/api/apis-proxy.ts +++ b/packages/chronicle/src/server/api/apis-proxy.ts @@ -1,4 +1,5 @@ import { defineHandler, HTTPError } from 'nitro'; +import { StatusCodes } from 'http-status-codes'; import { loadConfig } from '@/lib/config'; import { loadApiSpecs } from '@/lib/openapi'; @@ -27,12 +28,12 @@ function isPathSafe(p: string): boolean { export default defineHandler(async event => { if (event.req.method !== 'POST') { - throw new HTTPError({ status: 405, message: 'Method not allowed' }); + throw new HTTPError({ status: StatusCodes.METHOD_NOT_ALLOWED, message: 'Method not allowed' }); } const contentLength = parseInt(event.req.headers.get('content-length') ?? '0', 10); if (contentLength > MAX_BODY_SIZE) { - throw new HTTPError({ status: 413, message: `Request body too large (max ${MAX_BODY_SIZE} bytes)` }); + throw new HTTPError({ status: StatusCodes.CONTENT_TOO_LARGE, message: `Request body too large (max ${MAX_BODY_SIZE} bytes)` }); } const { specName, method, path, headers, body } = @@ -40,7 +41,7 @@ export default defineHandler(async event => { if (!specName || !method || !path) { throw new HTTPError({ - status: 400, + status: StatusCodes.BAD_REQUEST, message: 'Missing specName, method, or path' }); } @@ -50,11 +51,11 @@ export default defineHandler(async event => { const spec = specs.find(s => s.name === specName); if (!spec) { - throw new HTTPError({ status: 404, message: `Unknown spec: ${specName}` }); + throw new HTTPError({ status: StatusCodes.NOT_FOUND, message: `Unknown spec: ${specName}` }); } if (!isPathSafe(path)) { - throw new HTTPError({ status: 400, message: 'Invalid path' }); + throw new HTTPError({ status: StatusCodes.BAD_REQUEST, message: 'Invalid path' }); } const url = spec.server.url + path; @@ -86,14 +87,14 @@ export default defineHandler(async event => { }); } catch (error) { if (error instanceof DOMException && error.name === 'TimeoutError') { - throw new HTTPError({ status: 504, message: `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS}ms` }); + throw new HTTPError({ status: StatusCodes.GATEWAY_TIMEOUT, message: `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS}ms` }); } const message = error instanceof Error ? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}` : 'Request failed'; throw new HTTPError({ - status: 502, + status: StatusCodes.BAD_GATEWAY, message: `Could not reach ${url}\n${message}` }); }