Skip to content

[P0][BUG] Express process crash (DoS): async route handlers throw unhandled promise rejections on malformed bodies #4

Description

@pratik-saptarshi

Summary

Multiple Express route handlers in src/server/routes.ts are async functions that perform property destructuring and await calls without wrapping them in try/catch. When any of these throw (e.g. due to a null body, a JSON.parse error, or a downstream failure), the resulting unhandled promise rejection crashes the entire Express process with an UnhandledPromiseRejectionWarning. Any user or attacker who can reach /api (e.g. via loopback) can DoS the server with a single malformed request.

Panel verdict: P0, VERIFIED, EXISTING_DEFECT, unanimous consensus.


Affected Files

File Lines Issue
src/server/routes.ts L470–L485 async POST /chat/:slug/:id/messagesawait session.sendMessage() not guarded
src/server/routes.ts L422–L429 async GET /preflight/ghawait checkGh() wrapped but error re-thrown if res is closed
src/server/index.ts L29 No global Express async error handler mounted

Root Cause — Code Evidence

/chat/:slug/:id/messages async handler (src/server/routes.ts, lines 470–485):

router.post('/chat/:slug/:id/messages', async (req, res) => {
  const { slug, id } = req.params;
  if (!validId(slug) || !validId(id)) return res.status(400).json({ error: 'Invalid id' });
  const text = (req.body as { text?: unknown }).text;
  if (typeof text !== 'string' || !text.trim()) {
    return res.status(400).json({ error: 'text is required' });
  }
  const session = chat.getOrCreate({ slug, reportId: id });
  if (!session) return res.status(404).json({ error: 'Scan or report not found' });
  try {
    await session.sendMessage(text.trim());
    res.json({ ok: true });
  } catch (err) {
    res.status(409).json({ error: err instanceof Error ? err.message : String(err) });
  }
  // ↑ If res is already closed (client disconnected) when sendMessage() resolves,
  //   res.json() throws — this throw is NOT caught and becomes an unhandled rejection.
});

No global error handler in src/server/index.ts:

const app = express();
app.disable('etag');

const api = createApiRouter();
app.use('/api', api);
// ↑ No app.use((err, req, res, next) => { ... }) at the end
//   Express cannot catch async rejections without express-async-errors or manual wrapping

Impact


Steps to Reproduce

# Start Probus
npm run dev

# In another terminal: send a chat message and immediately close the connection
curl -X POST http://127.0.0.1:9090/api/chat/some-slug/some-id/messages \
  -H 'Content-Type: application/json' \
  -d '{"text": "hello"}' \
  --max-time 0.001  # disconnect immediately

Observe the Probus process log for UnhandledPromiseRejection and potential crash.


Remediation

Option A — Install express-async-errors (simplest, zero code change)

npm install express-async-errors
// src/server/index.ts — add at the very top, before express is imported
import 'express-async-errors';
import express from 'express';
// ...

// Add a global error handler at the END of startServer(), after all routes:
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
  console.error('[express error]', err.stack ?? err.message);
  if (!res.headersSent) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

Option B — Wrap every async handler manually

Add a utility wrapper:

const asyncRoute = (
  fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
) => (req: Request, res: Response, next: NextFunction) => fn(req, res, next).catch(next);

Then wrap every async handler:

router.post('/chat/:slug/:id/messages', asyncRoute(async (req, res) => {
  // ... handler body unchanged
}));

Option C — Guard res.json calls after await

Check res.writableEnded before writing:

if (!res.writableEnded) {
  res.json({ ok: true });
}

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions