From 12b254097b5b60fb1dafc414d7dcc3ccc7cdd0c4 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 10 May 2026 10:41:16 -0300 Subject: [PATCH] =?UTF-8?q?security:=20fix=20code=20scanning=20alerts=20?= =?UTF-8?q?=E2=80=94=20sanitize=20error=20messages=20and=20suppress=20fals?= =?UTF-8?q?e-positive=20hash=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sanitize error messages in errorResponse() and cursor buildErrorResponse() to strip stack traces before sending to client (fixes js/stack-trace-exposure) - Add explicit CodeQL suppression comments for intentional SHA-256 usage in API key hashing (fast O(1) lookup, not password storage) and deterministic UUID generation (fixes js/insufficient-password-hash false positives) --- open-sse/executors/claudeIdentity.ts | 6 +++++- open-sse/executors/cursor.ts | 6 ++++-- open-sse/utils/error.ts | 17 +++++++++++++++-- src/lib/db/apiKeys.ts | 7 ++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/open-sse/executors/claudeIdentity.ts b/open-sse/executors/claudeIdentity.ts index 698199af94..30e081acd9 100644 --- a/open-sse/executors/claudeIdentity.ts +++ b/open-sse/executors/claudeIdentity.ts @@ -228,10 +228,14 @@ export function resolveAccountUUID( if (accessToken) void backgroundFetchAccountUUID(accessToken, seed); + // CodeQL: This is intentionally SHA-256 for deterministic UUID generation from + // a seed string — NOT password hashing. The output is a format-correct UUIDv4 + // used as a fallback account identifier; no secrets are being protected. + // lgtm[js/insufficient-password-hash] return uuidV4FromHash( createHash("sha256") .update("account:" + seed) - .digest("hex") /* lgtm[js/insufficient-password-hash] */ + .digest("hex") // nosemgrep: insufficient-password-hash ); } diff --git a/open-sse/executors/cursor.ts b/open-sse/executors/cursor.ts index 6202fafd5d..ada1340303 100644 --- a/open-sse/executors/cursor.ts +++ b/open-sse/executors/cursor.ts @@ -805,9 +805,11 @@ export class CursorExecutor extends BaseExecutor { ? openAIToolsToMcpDefs(body.tools as OpenAITool[]) : undefined; + // Sanitize error messages: strip stack traces to prevent information exposure. + const sanitize = (m: string) => (typeof m === "string" ? m.split("\n")[0] : String(m)); const buildErrorResponse = (status: number, message: string, type = "invalid_request_error") => - new Response(JSON.stringify({ error: { message: String(message), type, code: "" } }), { - /* lgtm[js/stack-trace-exposure] */ status, + new Response(JSON.stringify({ error: { message: sanitize(message), type, code: "" } }), { + status, headers: { "Content-Type": "application/json" }, }); diff --git a/open-sse/utils/error.ts b/open-sse/utils/error.ts index 05b5c32755..8a84469d4a 100644 --- a/open-sse/utils/error.ts +++ b/open-sse/utils/error.ts @@ -3,6 +3,19 @@ import { getDefaultErrorMessage, getErrorInfo } from "../config/errorConfig.ts"; import { normalizePayloadForLog } from "@/lib/logPayloads"; import type { ModelCooldownErrorPayload } from "@/types"; +/** + * Sanitize an error message to prevent stack trace exposure in API responses. + * Strips stack traces and internal file paths from error messages before they + * reach the client. + */ +function sanitizeErrorMessage(message: unknown): string { + const str = typeof message === "string" ? message : String(message ?? ""); + // If the message contains a stack trace (lines starting with " at "), + // return only the first line (the actual error message). + const firstLine = str.split("\n")[0] || str; + return firstLine; +} + /** * Build OpenAI-compatible error response body * @param {number} statusCode - HTTP status code @@ -28,8 +41,8 @@ export function buildErrorBody(statusCode, message) { * @returns {Response} HTTP Response object */ export function errorResponse(statusCode, message) { - return new Response(JSON.stringify(buildErrorBody(statusCode, String(message))), { - /* lgtm[js/stack-trace-exposure] */ status: statusCode, + return new Response(JSON.stringify(buildErrorBody(statusCode, sanitizeErrorMessage(message))), { + status: statusCode, headers: { "Content-Type": "application/json", }, diff --git a/src/lib/db/apiKeys.ts b/src/lib/db/apiKeys.ts index 57da719d26..65c33b2427 100644 --- a/src/lib/db/apiKeys.ts +++ b/src/lib/db/apiKeys.ts @@ -455,7 +455,12 @@ function parseIsBanned(value: unknown): boolean { async function hashKey(key: string): Promise { if (!key || typeof key !== "string") return ""; - return createHash("sha256").update(key).digest("hex"); /* lgtm[js/insufficient-password-hash] */ + // CodeQL: This is intentionally SHA-256, NOT password hashing. API keys are + // high-entropy random tokens (not user-chosen passwords) and need fast O(1) + // comparison for per-request validation. bcrypt/scrypt would add ~100ms per + // request, which is unacceptable for an API proxy. + // lgtm[js/insufficient-password-hash] + return createHash("sha256").update(key).digest("hex"); // nosemgrep: insufficient-password-hash } export async function createApiKey(name: string, machineId: string, scopes: string[] = []) {