Skip to content

Commit ea2f2a3

Browse files
pi0claude
andcommitted
fix(json-rpc): do not leak internal exception messages to clients
When a JSON-RPC method handler throws a non-HTTPError (an unexpected internal exception), the raw error message was copied into the response `data` field and returned to the caller, leaking internal details (DB driver errors, file paths, etc.). The unhandled branch now omits `data`, so clients receive only `{ code: -32603, message: "Internal error" }`, consistent with `HTTPError.toJSON()` masking `data`/`message` for unhandled errors. Thrown `HTTPError`s still surface their opt-in `data`/`message`. Co-Authored-By: Claude Fable 5 <[email protected]>
1 parent e965604 commit ea2f2a3

3 files changed

Lines changed: 62 additions & 7 deletions

File tree

src/utils/json-rpc.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,11 @@ async function processJsonRpcMethod<C extends H3Event | WebSocketPeer>(
319319
: {
320320
status: 500,
321321
message: "Internal error",
322-
data:
323-
error_ != null && typeof error_ === "object" && "message" in error_
324-
? error_.message
325-
: undefined,
322+
// Never expose internal exception details to untrusted callers.
323+
// Consistent with `HTTPError.toJSON()` hiding `data`/`message` for
324+
// unhandled errors (see `src/error.ts`). Thrown `HTTPError`s keep
325+
// their opt-in `data`/`message` via the branch above.
326+
data: undefined,
326327
};
327328
const statusCode = h3Error.status;
328329
const statusMessage = h3Error.message;

test/json-rpc-ws.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ describe("defineJsonRpcWebSocketHandler", () => {
261261
expect(JSON.parse(sent[0])).toEqual({
262262
jsonrpc: "2.0",
263263
id: 4,
264-
error: { code: -32_603, message: "Internal error", data: "Handler error" },
264+
error: { code: -32_603, message: "Internal error" },
265265
});
266266
});
267267

test/json-rpc.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ describeMatrix("json-rpc", (t, { describe, it, expect }) => {
3131
errorPrimitive: () => {
3232
throw "Primitive error";
3333
},
34+
// Simulates an unexpected internal exception whose message must not leak.
35+
leaky: () => {
36+
throw new Error("secret internal detail");
37+
},
38+
httpErrorWithData: () => {
39+
throw new HTTPError({
40+
status: 422,
41+
message: "Validation failed",
42+
data: { field: "email" },
43+
});
44+
},
3445
// "constructor" is a valid method name — the null-prototype map
3546
// ensures it doesn't resolve to Object.prototype.constructor.
3647
constructor: () => {
@@ -256,14 +267,58 @@ describeMatrix("json-rpc", (t, { describe, it, expect }) => {
256267
expect(json).toEqual({
257268
error: {
258269
code: -32_603,
259-
data: "Handler error",
260270
message: "Internal error",
261271
},
262272
id: 4,
263273
jsonrpc: "2.0",
264274
});
265275
});
266276

277+
it("should not leak internal error details in error data", async () => {
278+
t.app.post("/json-rpc", eventHandler);
279+
const result = await t.fetch("/json-rpc", {
280+
method: "POST",
281+
body: JSON.stringify({
282+
jsonrpc: "2.0",
283+
method: "leaky",
284+
id: 42,
285+
}),
286+
});
287+
const json = await result.json();
288+
// Unexpected internal exceptions must not surface their message to clients.
289+
expect(json).toEqual({
290+
error: {
291+
code: -32_603,
292+
message: "Internal error",
293+
},
294+
id: 42,
295+
jsonrpc: "2.0",
296+
});
297+
expect(JSON.stringify(json)).not.toContain("secret internal detail");
298+
});
299+
300+
it("should still surface data for thrown HTTPError (developer opt-in)", async () => {
301+
t.app.post("/json-rpc", eventHandler);
302+
const result = await t.fetch("/json-rpc", {
303+
method: "POST",
304+
body: JSON.stringify({
305+
jsonrpc: "2.0",
306+
method: "httpErrorWithData",
307+
id: 43,
308+
}),
309+
});
310+
const json = await result.json();
311+
expect(json).toEqual({
312+
error: {
313+
code: -32_602,
314+
data: { field: "email" },
315+
message: "Validation failed",
316+
},
317+
id: 43,
318+
jsonrpc: "2.0",
319+
});
320+
});
321+
267322
it("should handle primitive thrown errors and map to JSON-RPC error", async () => {
268323
t.app.post("/json-rpc", eventHandler);
269324
const result = await t.fetch("/json-rpc", {
@@ -676,7 +731,6 @@ describeMatrix("json-rpc", (t, { describe, it, expect }) => {
676731
error: {
677732
code: -32_603,
678733
message: "Internal error",
679-
data: "Handler error",
680734
},
681735
});
682736
});

0 commit comments

Comments
 (0)