Repository navigation
ctx7-2586 sanitize error responses for rejected request bodies - #3142
ramazanoacar merged 5 commits into
Conversation
| // body-parser also rejects bodies that are too large or undecodable, each | ||
| // with its own 4xx. Keep that status: a 500 would invite the client to | ||
| // retry a request that can never succeed. | ||
| const rejectedBodyStatus = (err: unknown): number | undefined => { |
There was a problem hiding this comment.
rejectedBodyStatus accepts every error carrying a 4xx status, not specifically body-parser errors. A future route error such as a deliberate 401/403 would therefore be mislabeled as a rejected body and converted to JSON-RPC Invalid Request. Can we keep the body parser and its error boundary adjacent (or explicitly identify parser error types), then leave a separate generic handler for application errors?
| console.error(`Unhandled request error: ${where}`, err); | ||
| } | ||
|
|
||
| if (res.headersSent) { |
There was a problem hiding this comment.
Returning when res.headersSent silently consumes the error. If a route or stream fails after starting its response, the connection can remain open indefinitely. Please accept next and use return next(err) here so Express’s default handler can close the connection.
fahreddinozcan
left a comment
There was a problem hiding this comment.
The reported malformed-JSON leak is fixed and runtime validation passes (local server exercised; 64/64 MCP tests, typecheck, lint, and formatting all pass). I am requesting changes for the two inline correctness/boundary issues: scope rejected-body handling to errors originating from express.json() instead of classifying every 4xx application error as a body rejection, and delegate with next(err) when headers have already been sent. The clean structure is a parser-local error boundary plus a small generic terminal 500 handler.
| next(); | ||
| }); | ||
|
|
||
| app.use(express.json()); |
There was a problem hiding this comment.
This fixes a globally mounted parser by introducing globally mounted JSON-RPC error handling, but JSON-RPC is only owned by /mcp. The other current endpoints do not consume JSON bodies and should neither parse them nor return MCP error envelopes.
Can we make the ownership boundary explicit by extracting the handler to something like src/lib/mcp-body-error-handler.ts and mounting both it and express.json() on an MCP router?
import { mcpBodyErrorHandler } from "./lib/mcp-body-error-handler.js";
const mcpRouter = express.Router();
mcpRouter.use(express.json());
mcpRouter.use(mcpBodyErrorHandler);
mcpRouter.all("/", (req, res) => handleMcpRequest(req, res, false));
mcpRouter.all("/oauth", (req, res) => handleMcpRequest(req, res, true));
app.use("/mcp", mcpRouter);This addresses the pentest across the whole application: MCP requests receive sanitized JSON-RPC parser errors, while health, discovery, challenge, and unknown routes never unnecessarily enter the JSON parser. It also keeps the parser/error mechanics out of an already busy index.ts.
handleMcpRequest already sanitizes downstream MCP failures, so the application-wide terminalErrorHandler appears unnecessary. Please also add a regression assertion that malformed JSON sent to a non-MCP path follows that route's normal contract rather than returning JSON-RPC.
627412f to
a5f9dd7
Compare
fahreddinozcan
left a comment
There was a problem hiding this comment.
Re-reviewed the latest head. The JSON parser and dedicated error handler are now scoped to mcpRouter, keeping the behavior isolated to /mcp while leaving the rest of the app unchanged. I also validated the malformed-body behavior against the local server and ran the package checks/tests successfully. This looks good now.
…6-return-sanitized-errors-for-malformed-requests
|
Merged
Neither side is wrong on its own; the combination is. Git auto-merged the region Fix is to compare the mount-aware path, which is correct under both layouts: return `${req.baseUrl}${req.path}` === "/mcp/oauth" || Boolean(plugin); |
Problem
express.json()sits ahead of the routes, so a rejected body throws insidebody-parser and calls
next(err). With no error handler registered, therequest skipped CORS, both
/mcproutes (including the/mcp/oauthauthcheck) and the catch-all 404, and fell into Express's default handler.
Before
Leaks absolute paths, dependency versions and the parser's message. Because the
auth check is skipped too, an unauthenticated caller gets the same trace from
/mcp/oauth.After
Identical on
/mcpand/mcp/oauth.Changes
A terminal error handler after the catch-all 404:
entity.parse.failed-32700"Parse error"entity.too.large)-32600"Invalid Request"-32603Messages are fixed strings, since body-parser's own text quotes the body. Only
entity.parse.failedcounts as a parse error, so an unrelatedSyntaxErrorfrom application code is not reclassified. Keeping the middle tier's status
matters: reporting an oversized body as a 500 would invite the client to retry
a request that can never succeed.
CORS moved above
express.json()so error responses carryAccess-Control-Allow-Origin— previously they did not, and browser clientssaw a CORS failure instead of the status. Preflight still returns 200.
Logs carry method, path,
content-lengthand body-parser's error tag only —never the body or the headers, which hold the API key.
Preserved
Valid JSON (
-32600), empty body (-32600), wrongContent-Type(415
-32000),/mcp/oauthwithout credentials (401-32001),/ping,unknown routes and
OPTIONSare all unchanged. Empty bodies are worth noting:body-parser treats them as
{}rather than a parse failure.Tests
test/integration.test.ts, on the existing built-binary harness so theassertions cover the real middleware order.
stack trace, HTML, path, dependency name, parser message, submitted body or
credential in the payload.
-32600.Confirmed failing before the fix, passing after.
Verification