diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a58dd10fe..964b2b8e7 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -6,3 +6,8 @@ **Vulnerability:** API routes were returning internal server exceptions and stack traces directly to the client via `HTTPException(..., detail=str(e))`. **Learning:** Developers often unintentionally leak sensitive deployment context (e.g., paths, database errors) when relying on generic exception catching blocks. **Prevention:** Hardcode static error strings for unexpected 500 exceptions (e.g., `detail="Internal server error"`) while ensuring the full exception trace is securely logged server-side. Every sanitized 500 handler in `router.py`, `main.py`, and mounted routers (e.g. `reporting_routes.py`) now logs via `logger.error(..., exc_info=True)` so the traceback is preserved for internal monitoring without ever reaching the client. Guard against regressions with tests that assert the response body equals the generic message AND excludes the raised exception string (status-code-only assertions are insufficient). + +## 2026-08-03 - Prevent Information Disclosure in API Error Responses +**Vulnerability:** API routes were returning internal stack traces directly to the client by conditionally evaluating `error instanceof Error ? error.message : String(error)` in catch blocks. +**Learning:** Returning `error.message` directly from generic catch blocks can inadvertently expose sensitive deployment context (e.g. file paths, internal service failures, API keys). +**Prevention:** Hardcode static error strings or rely on the sanitized utility `formatApiError(error).message` when constructing API response payloads. Use the raw error only for server-side logic checks, logs, or debugging. diff --git a/apps/web/src/app/api/agents/actions/route.ts b/apps/web/src/app/api/agents/actions/route.ts index ddcdbf1d0..23fa4b922 100644 --- a/apps/web/src/app/api/agents/actions/route.ts +++ b/apps/web/src/app/api/agents/actions/route.ts @@ -56,18 +56,21 @@ export async function POST(request: Request): Promise { }); } catch (error) { console.error('Action agent error:', error); - const message = error instanceof Error ? error.message : String(error); + const rawMessage = error instanceof Error ? error.message : String(error); // Only the agent's own validation/config guards are client errors (400). // Match exact phrases so an upstream provider error that merely mentions // "API key" (e.g. OpenAI's 401 "Incorrect API key provided") is correctly // surfaced as an upstream failure (502), not mislabeled as a bad request. const isClientError = - message.startsWith('No AI API key configured') || - message.includes('transcript is too short'); + rawMessage.startsWith('No AI API key configured') || + rawMessage.includes('transcript is too short'); + + // SECURITY: Prevent leaking internal stack traces or error text on 5xx failures. + const safeError = isClientError ? rawMessage : 'Internal server error'; return NextResponse.json( - { success: false, error: message, actions: [] }, + { success: false, error: safeError, actions: [] }, { status: isClientError ? 400 : 502 }, ); } diff --git a/apps/web/src/app/api/extract-events/route.ts b/apps/web/src/app/api/extract-events/route.ts index 8eb8f8b56..ca7e6b69a 100644 --- a/apps/web/src/app/api/extract-events/route.ts +++ b/apps/web/src/app/api/extract-events/route.ts @@ -255,12 +255,11 @@ Respond with ONLY valid JSON matching the required structure.`; return NextResponse.json({ success: true, provider, data: parsed }); } catch (error) { console.error('Event extraction error:', error); - const message = error instanceof Error ? error.message : String(error); - + // SECURITY: Prevent information disclosure by masking the raw error message return NextResponse.json({ success: false, - error: message, + error: 'Internal server error', data: { events: [], actions: [], summary: '', topics: [] }, - }); + }, { status: 500 }); } } diff --git a/apps/web/src/app/api/jobs/[jobId]/route.ts b/apps/web/src/app/api/jobs/[jobId]/route.ts index c9da65176..5c90255f2 100644 --- a/apps/web/src/app/api/jobs/[jobId]/route.ts +++ b/apps/web/src/app/api/jobs/[jobId]/route.ts @@ -38,8 +38,10 @@ export async function GET( headers: { 'Content-Type': 'application/json' }, }); } catch (error) { + console.error('Job fetch error:', error); + // SECURITY: Prevent information disclosure by masking the raw error message return NextResponse.json( - { error: error instanceof Error ? error.message : String(error) }, + { error: 'Internal server error' }, { status: 502 }, ); } diff --git a/apps/web/src/app/api/search/route.ts b/apps/web/src/app/api/search/route.ts index 96d40b62d..e95f32dd3 100644 --- a/apps/web/src/app/api/search/route.ts +++ b/apps/web/src/app/api/search/route.ts @@ -43,8 +43,9 @@ export async function POST(req: NextRequest) { }); } catch (error) { console.error('Upstash search error:', error); + // SECURITY: Prevent information disclosure by masking the raw error message return NextResponse.json( - { error: 'search_failed', detail: error instanceof Error ? error.message : String(error) }, + { error: 'search_failed', detail: 'Internal server error' }, { status: 502 }, ); } @@ -107,8 +108,9 @@ export async function PUT(req: NextRequest) { }); } catch (error) { console.error('Upstash upsert error:', error); + // SECURITY: Prevent information disclosure by masking the raw error message return NextResponse.json( - { error: 'upsert_failed', detail: error instanceof Error ? error.message : String(error) }, + { error: 'upsert_failed', detail: 'Internal server error' }, { status: 502 }, ); }