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.
This commit is contained in:
diegosouzapw
2026-05-14 14:46:49 -03:00
parent 67f713d3a5
commit f3f1f9f36e
4 changed files with 34 additions and 10 deletions

View File

@@ -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++;
}
}

View File

@@ -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<string, number> = {
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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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");
}
});