Skip to content

fix(gateway): don't leak error details on unhandled 500 - #104

Merged
dylanneve1 merged 3 commits into
mainfrom
fix/gateway-stack-trace-exposure
May 7, 2026
Merged

fix(gateway): don't leak error details on unhandled 500#104
dylanneve1 merged 3 commits into
mainfrom
fix/gateway-stack-trace-exposure

Conversation

@claudiusthebot

Copy link
Copy Markdown
Collaborator

Summary

Fixes CodeQL alert #4 (js/stack-trace-exposure, medium) on src/core/gateway.ts.

The catch-all handler at the bottom of the request lifecycle was returning err.message directly to the HTTP client. The gateway only listens on 127.0.0.1 so the real attack surface is small, but errors can still propagate into logs/transcripts that flow further out, and CodeQL is flagging it on every main commit.

Changes

  • Server side: log the full error (incl. stack) via logError("gateway", ...).
  • Client side: return a generic "Internal server error".
  • The structured handler-level error path inside handleAction is unchanged β€” those errors are part of the action protocol (e.g. "No active chat context", "send_message: ...") and tests assert their content. Only the truly-unexpected outer 500 is sanitized.

Test plan

  • npx vitest run src/__tests__/gateway-http.test.ts β€” 24 passed
  • npx vitest run β€” full suite, 1635/1635 passed
  • npx tsc --noEmit β€” clean
  • npm run lint β€” 0 errors (9 pre-existing warnings)
  • The test that hits this path (returns 500 when handleAction result cannot be JSON-serialized) only asserts body.error is truthy β€” still holds.
  • After merge: confirm CodeQL alert chore(deps): Bump actions/upload-artifact from 4 to 7Β #4 is auto-closed on next scan.

πŸ€– Heartbeat agent β€” picked up from #45's "next time: investigate vulnerabilities Dylan flagged on main"

The catch-all in the gateway HTTP handler returned `err.message` directly
to the client, which CodeQL flags as `js/stack-trace-exposure` (alert #4
on main). The gateway listens on 127.0.0.1 only so the practical attack
surface is small, but error messages can still surface to logs/transcripts
that flow further out.

- Log the full error (with stack) server-side via logError().
- Return a generic "Internal server error" to the client.

The structured handler-level error path (handleAction) is unchanged β€”
those errors are part of the action protocol and tests assert their
content. Only the truly-unexpected outer 500 is sanitized.

All 1635 tests pass; the existing test that triggers this path
(`returns 500 when handleAction result cannot be JSON-serialized`)
only asserts `body.error` is truthy, which still holds.
@claudiusthebot
claudiusthebot requested a review from dylanneve1 as a code owner May 3, 2026 14:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a CodeQL js/stack-trace-exposure alert by sanitizing the gateway’s catch-all HTTP 500 error response so unexpected server errors no longer return internal error details to the client.

Changes:

  • Log unexpected request-lifecycle errors server-side via logError("gateway", ...).
  • Replace the client-facing 500 JSON response error with a generic "Internal server error" message.

πŸ’‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core/gateway.ts Outdated
Comment on lines +277 to +278
`Unhandled error on ${req.method} ${req.url}: ${detail}`,
err,
Address Copilot review feedback on PR #104: logError previously only
captured err.message, dropping the stack. Enhance logError to include
err.stack as a separate structured field when err is an Error.

- pino-pretty renders the stack field on its own line for readability
- JSON log consumers can read err (concise) or stack (full trace)
- Benefits all 234 logError callsites, not just gateway

Simplify the gateway catch block accordingly: drop the manual
detail = err.stack ?? err.message interpolation in the message string,
since the structured stack field now carries it.

Updated log.test.ts assertion to expect both err and stack fields.
1635/1635 tests pass, tsc clean, lint 0 errors, prettier clean.
@claudiusthebot

Copy link
Copy Markdown
Collaborator Author

Good catch from Copilot β€” logError was only capturing err.message, dropping the stack from the structured context.

Pushed c270403 to address it. Took the second of Copilot's two suggestions (enhance logError) since it benefits all 234 callsites in the codebase, not just the gateway:

if (err instanceof Error) {
  logger.error({ component, err: err.message, stack: err.stack }, message);
}

pino-pretty renders the stack field on its own line; JSON consumers can read either field. Then dropped the manual detail = err.stack ?? err.message interpolation in the gateway catch block since the structured stack field now carries it β€” back to a clean logError("gateway", "Unhandled error on ...", err).

Updated src/__tests__/log.test.ts to assert both err and stack are present.

Verified locally:

  • npx vitest run β€” 1635/1635 passed
  • npx tsc --noEmit β€” clean
  • npm run lint β€” 0 errors
  • npx prettier --check β€” clean

Diff vs 84d2c1d: 3 files, 12+/9βˆ’.

@dylanneve1
dylanneve1 merged commit 5a359db into main May 7, 2026
13 checks passed
@dylanneve1
dylanneve1 deleted the fix/gateway-stack-trace-exposure branch May 7, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants