fix: prevent error message information disclosure + standardize error handling (TDD)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m10s

Security & Consistency Fixes:
- Convert routes no longer leak internal error messages (err.message)
- Templates route no longer exposes error details via 'detail' field
- Admin cleanup endpoint no longer exposes error message
- Standardized QUEUE_FULL response: 429 → 503 (Service Unavailable)
- Added missing PDF_TIMEOUT handling: returns 504 Gateway Timeout
- Generic 500 errors now return 'PDF generation failed.' without internals

TDD Approach:
1. RED: Created error-responses.test.ts with 11 failing tests
2. GREEN: Fixed src/routes/convert.ts, templates.ts, and index.ts
3. Updated convert.test.ts to expect new correct status codes
4. All 541 tests pass

Before: 'PDF generation failed: Puppeteer crashed: SIGSEGV in Chrome'
After:  'PDF generation failed.' (internals logged, not exposed)

Closes security audit findings re: information disclosure
This commit is contained in:
OpenClaw Agent 2026-03-07 17:05:54 +01:00
parent 6b1b3d584e
commit 424a16ed8a
5 changed files with 293 additions and 13 deletions

View file

@ -125,10 +125,14 @@ convertRouter.post("/html", async (req: Request & { acquirePdfSlot?: () => Promi
} catch (err: any) {
logger.error({ err }, "Convert HTML error");
if (err.message === "QUEUE_FULL") {
res.status(429).json({ error: "Server busy - too many concurrent PDF generations. Please try again in a few seconds." });
res.status(503).json({ error: "Server busy — too many concurrent PDF generations. Please try again in a few seconds." });
return;
}
res.status(500).json({ error: `PDF generation failed: ${err.message}` });
if (err.message === "PDF_TIMEOUT") {
res.status(504).json({ error: "PDF generation timed out." });
return;
}
res.status(500).json({ error: "PDF generation failed." });
} finally {
if (slotAcquired && req.releasePdfSlot) {
req.releasePdfSlot();
@ -227,10 +231,14 @@ convertRouter.post("/markdown", async (req: Request & { acquirePdfSlot?: () => P
} catch (err: any) {
logger.error({ err }, "Convert MD error");
if (err.message === "QUEUE_FULL") {
res.status(429).json({ error: "Server busy - too many concurrent PDF generations. Please try again in a few seconds." });
res.status(503).json({ error: "Server busy — too many concurrent PDF generations. Please try again in a few seconds." });
return;
}
res.status(500).json({ error: `PDF generation failed: ${err.message}` });
if (err.message === "PDF_TIMEOUT") {
res.status(504).json({ error: "PDF generation timed out." });
return;
}
res.status(500).json({ error: "PDF generation failed." });
} finally {
if (slotAcquired && req.releasePdfSlot) {
req.releasePdfSlot();
@ -360,10 +368,14 @@ convertRouter.post("/url", async (req: Request & { acquirePdfSlot?: () => Promis
} catch (err: any) {
logger.error({ err }, "Convert URL error");
if (err.message === "QUEUE_FULL") {
res.status(429).json({ error: "Server busy - too many concurrent PDF generations. Please try again in a few seconds." });
res.status(503).json({ error: "Server busy — too many concurrent PDF generations. Please try again in a few seconds." });
return;
}
res.status(500).json({ error: `PDF generation failed: ${err.message}` });
if (err.message === "PDF_TIMEOUT") {
res.status(504).json({ error: "PDF generation timed out." });
return;
}
res.status(500).json({ error: "PDF generation failed." });
} finally {
if (slotAcquired && req.releasePdfSlot) {
req.releasePdfSlot();