From fcf4ed303990e1609f2b2c3f278b5d8d5cdba808 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Wed, 5 Aug 2026 19:42:49 +0530 Subject: [PATCH] docs: close a prototype-chain authorization bypass in the plan server.ts looked procedures up with plain property indexing, so every Object.prototype member resolved as truthy. A prototype member carries no `permission`, so the permission gate was skipped entirely. Verified: with a contract whose only procedure declares a permission and a checkPermission that always denies, invoke("add") correctly returns RPC_DENIED, while invoke("constructor") returns {"ok":true,"value":{"a":2}} and the gate never runs. Reachable over the wire as POST /__wrnexus/rpc//constructor by anything that clears the internal-caller check - i.e. any workspace app. Fixed at both layers: Object.hasOwn for the procedure and handler lookups, and a character-class guard on the path segments before they are used as lookup keys. Co-Authored-By: Claude Opus 5 --- ...026-08-05-inter-app-comms-implementation.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-08-05-inter-app-comms-implementation.md b/docs/plans/2026-08-05-inter-app-comms-implementation.md index 46245b9a..f62afbac 100644 --- a/docs/plans/2026-08-05-inter-app-comms-implementation.md +++ b/docs/plans/2026-08-05-inter-app-comms-implementation.md @@ -1460,8 +1460,14 @@ export function implement( contract, async invoke(procedureName, payload, identity) { - const def = contract.procedures[procedureName as keyof Procedures]; - const handler = handlers[procedureName as keyof Procedures]; + // Object.hasOwn, not plain indexing: "constructor", "toString" and every + // other Object.prototype member otherwise resolve as truthy, and a + // prototype member carries no `permission`, so the gate below is skipped + // entirely and an unintended function runs with attacker-controlled input. + const known = + Object.hasOwn(contract.procedures, procedureName) && Object.hasOwn(handlers, procedureName); + const def = known ? contract.procedures[procedureName as keyof Procedures] : undefined; + const handler = known ? handlers[procedureName as keyof Procedures] : undefined; if (!def || !handler) { return failure(RPC_ERROR_CODES.unknown, `No procedure '${contract.name}/${procedureName}'`); } @@ -2313,8 +2319,12 @@ export async function handleRpcRequest( } const [, , , serviceName, procedureName] = url.pathname.split("/"); - const service = serviceName ? services.get(serviceName) : undefined; - if (!service || !procedureName) { + // Constrain the segment before it is used as a lookup key, so a prototype + // member can never be reached even if a future implement() regresses. + const SAFE_SEGMENT = /^[A-Za-z0-9_-]+$/; + const service = + serviceName && SAFE_SEGMENT.test(serviceName) ? services.get(serviceName) : undefined; + if (!service || !procedureName || !SAFE_SEGMENT.test(procedureName)) { return json({ ok: false, code: "RPC_UNKNOWN", message: "Unknown procedure", retryable: false }); }