From f3f1f9f36ed7e5fc7d9a536dc3d9816fd9a7033a Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 14 May 2026 14:46:49 -0300 Subject: [PATCH] fix(api): sanitize error responses in management routes Prevent raw exception messages from leaking stack frames or absolute paths in the console logs and token health endpoints. Also harden the i18n mirror move script by replacing shell-based git commands with execFileSync and a safer fallback for untracked files. --- scripts/docs/move-i18n-mirrors.mjs | 20 +++++++++++-------- src/app/api/logs/console/route.ts | 6 +++++- src/app/api/token-health/route.ts | 6 +++++- tests/unit/error-message-sanitization.test.ts | 12 +++++++++++ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/scripts/docs/move-i18n-mirrors.mjs b/scripts/docs/move-i18n-mirrors.mjs index 110f3dfda2..444e99946d 100644 --- a/scripts/docs/move-i18n-mirrors.mjs +++ b/scripts/docs/move-i18n-mirrors.mjs @@ -18,7 +18,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, "..", ".."); @@ -116,19 +116,23 @@ for (const locale of fs.readdirSync(I18N_DIR)) { } if (!fs.existsSync(subDir)) fs.mkdirSync(subDir, { recursive: true }); + const relSrc = path.relative(ROOT, src); + const relDst = path.relative(ROOT, dst); try { - execSync(`git mv -k -- "${path.relative(ROOT, src)}" "${path.relative(ROOT, dst)}"`, { + execFileSync("git", ["mv", "-k", "--", relSrc, relDst], { cwd: ROOT, stdio: "pipe", }); moved++; - } catch (e) { - // fallback: copy + delete + } catch { + // fallback: copy + delete; emulate `|| true` for the rm by ignoring its failure fs.renameSync(src, dst); - execSync( - `git rm --cached -- "${path.relative(ROOT, src)}" 2>/dev/null || true; git add -- "${path.relative(ROOT, dst)}"`, - { cwd: ROOT, stdio: "pipe", shell: "/bin/bash" } - ); + try { + execFileSync("git", ["rm", "--cached", "--", relSrc], { cwd: ROOT, stdio: "pipe" }); + } catch { + // file may not be tracked yet — safe to ignore + } + execFileSync("git", ["add", "--", relDst], { cwd: ROOT, stdio: "pipe" }); moved++; } } diff --git a/src/app/api/logs/console/route.ts b/src/app/api/logs/console/route.ts index cdfe0984dc..1ebd324b26 100644 --- a/src/app/api/logs/console/route.ts +++ b/src/app/api/logs/console/route.ts @@ -14,6 +14,7 @@ import { NextRequest, NextResponse } from "next/server"; import { readFileSync, existsSync } from "fs"; import { getAppLogFilePath } from "@/lib/logEnv"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; const LEVEL_ORDER: Record = { trace: 5, @@ -114,6 +115,9 @@ export async function GET(req: NextRequest) { }, }); } catch (err: any) { - return NextResponse.json({ error: err.message || "Failed to read logs" }, { status: 500 }); + return NextResponse.json( + { error: sanitizeErrorMessage(err?.message) || "Failed to read logs" }, + { status: 500 } + ); } } diff --git a/src/app/api/token-health/route.ts b/src/app/api/token-health/route.ts index f21abcc5e7..b1176aefd3 100644 --- a/src/app/api/token-health/route.ts +++ b/src/app/api/token-health/route.ts @@ -6,6 +6,7 @@ */ import { getProviderConnections } from "@/lib/localDb"; +import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; export async function GET() { try { @@ -31,6 +32,9 @@ export async function GET() { status: errored > 0 ? "error" : healthy < total ? "warning" : "healthy", }); } catch (err) { - return Response.json({ error: err.message, status: "unknown" }, { status: 500 }); + return Response.json( + { error: sanitizeErrorMessage((err as Error)?.message), status: "unknown" }, + { status: 500 } + ); } } diff --git a/tests/unit/error-message-sanitization.test.ts b/tests/unit/error-message-sanitization.test.ts index a977bf8fb6..e855d0dde1 100644 --- a/tests/unit/error-message-sanitization.test.ts +++ b/tests/unit/error-message-sanitization.test.ts @@ -239,3 +239,15 @@ test("buildErrorBody never exposes stack traces in its message", async () => { assert.equal(body.error.message, "Internal error"); assert.ok(!body.error.message.includes("at /opt")); }); + +test("GET /token-health response never leaks stack frames or absolute paths", async () => { + const tokenHealthRoute = await import("../../src/app/api/token-health/route.ts"); + const res = await tokenHealthRoute.GET(); + const body = (await res.json()) as any; + assert.ok(!("stack" in body), "response must not contain stack trace"); + if (typeof body.error === "string") { + assert.ok(!body.error.includes(" at "), "stack frame must not leak in error"); + assert.ok(!/^\//.test(body.error), "absolute POSIX path must not leak"); + assert.ok(!/^[A-Za-z]:[\\/]/.test(body.error), "absolute Windows path must not leak"); + } +});