Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@
**Vulnerability:** External network requests made via `fetch` lacked a timeout, allowing them to hang indefinitely if the remote server was slow or unresponsive. This could exhaust application resources (e.g., memory, sockets, thread pool limits) over time, leading to a Denial of Service (DoS).
**Learning:** Unbounded network requests tie up valuable system resources while waiting for a response that may never arrive.
**Prevention:** When implementing external API requests using `fetch` or similar HTTP clients, always enforce a timeout (e.g., using `AbortSignal.timeout(5000)`) to prevent unbounded waiting and application unresponsiveness.

* **Information Disclosure in API Errors**: When returning errors from the backend to the client, avoid passing the raw `.message` property of system or internal errors for 500-level statuses. Instead, sanitize 500-level errors by substituting a generic fallback message like `"Internal Server Error"` to prevent exposing sensitive internal state (e.g., file paths, IPs, connection strings, API keys) which might be contained in the raw error message.
16 changes: 14 additions & 2 deletions server/utils/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('utils/errors', () => {
expect(isAppError(new Error('x'))).toBe(false);
});

it('serializes AppError payload', () => {
it('serializes AppError payload and preserves message for < 500 status', () => {
const error = new AppError(400, 'bad', 'BAD', false, { field: 'x' });

expect(toErrorPayload(error)).toEqual({
Expand All @@ -28,6 +28,18 @@ describe('utils/errors', () => {
});
});

it('serializes AppError payload and sanitizes message for >= 500 status', () => {
const error = new AppError(500, 'database connection failed: secret_password', 'DB_ERROR', false, { field: 'x' });

expect(toErrorPayload(error)).toEqual({
error: 'Internal Server Error',
code: 'DB_ERROR',
status: 500,
retryable: false,
context: { field: 'x' },
});
});

it('maps rate-limit errors', () => {
expect(toErrorPayload(new Error('429 rate limit'))).toEqual({
error: 'Rate limit exceeded',
Expand Down Expand Up @@ -76,4 +88,4 @@ describe('utils/errors', () => {
retryable: false,
});
});
});
});
2 changes: 1 addition & 1 deletion server/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function isAppError(err: unknown): err is AppError {
export function toErrorPayload(err: unknown) {
if (isAppError(err)) {
return {
error: err.message,
error: err.status >= 500 ? 'Internal Server Error' : err.message,
code: err.code,
status: err.status,
retryable: err.retryable,
Expand Down
Loading