Skip to content
Merged
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 7 additions & 4 deletions apps/web/src/app/api/agents/actions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,21 @@ export async function POST(request: Request): Promise<NextResponse> {
});
} 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 },
);
}
Expand Down
7 changes: 3 additions & 4 deletions apps/web/src/app/api/extract-events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
4 changes: 3 additions & 1 deletion apps/web/src/app/api/jobs/[jobId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
);
}
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
);
}
Expand Down Expand Up @@ -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 },
);
}
Expand Down
Loading