Audit report AR-56

Reported privately in an independent security review, 2026-09-27.

Report date
2026-09-27
Kind
Bug
Status on master
Present
Proposed issue state
Open

Summary

When -rpcwhitelist stops a user from calling a method, the RPC server reports it only as HTTP status 403 with an empty body. The new IPC path in bitcoin-cli (v32) receives the body but not the status, so it prints a confusing JSON type error instead of a permission error. The report is accurate. It is a wrong error message, not a stability bug: nothing crashes, hangs or runs that should not. The report proposes changing the IPC interface; fixing the server so that a denial carries a normal JSON-RPC error looks like the better change.

The report

bitcoin-cli now tries the node's local IPC socket before HTTP. Over IPC it calls Rpc.executeRpc, which runs the request through the same code as the HTTP server, ExecuteHTTPRPC(). For a method excluded by -rpcwhitelist, that function sets status 403 and returns a null body. RpcImpl::executeRpc() returns only the body, so bitcoin-cli tries to read null as a reply object and reports a type error. Over HTTP, the same call reports "server returned HTTP error 403".

The report rates it low. It needs a configured whitelist and access to the local IPC socket. It changes only the error the requesting user sees: the whitelist is not bypassed and the denied method does not run.

Proposed change

Return the emulated HTTP status from executeRpc as a new result field, keep the reply text at its existing position on the wire, and have bitcoin-cli check the status before parsing the reply:

 interface Rpc $Proxy.wrap("interfaces::Rpc") {
-    executeRpc @0 (context :Proxy.Context, request :Text, uri :Text, user :Text) -> (result :Text);
+    executeRpc @0 (context :Proxy.Context, request :Text, uri :Text, user :Text) -> (response :Text, result :Int32);
 }

The libmultiprocess code generator maps a field named result to the C++ return value, so the text field is renamed to response. Already-built peers stay compatible on the wire, but code built from the schema has to use the new name, and the C++ interfaces::Rpc::executeRpc method now returns the status and writes the body to an output argument. An older server sends no status, which reads as zero and keeps the previous behaviour. The tests add a user restricted by -rpcwhitelist and compare IPC and HTTP for a denied method, an allowed one and an unknown one. The report considered a nested result struct (breaks old peers on the wire) and a separate versioned method (needs capability negotiation).

Assessment

Accurate. On Bitcoin Core master, all three whitelist checks in ExecuteHTTPRPC() set status 403 and return an empty value, and RpcImpl::executeRpc() drops the status. It only affects users who configure -rpcwhitelist and run a v32 bitcoin-cli that reaches the IPC socket, which it does by default.

The root cause is on the server side rather than in the IPC interface. The whitelist denial is the only RPC error that has no JSON-RPC error object in the body. It also returns HTTP 403 for JSON-RPC 2.0 requests, which the server's own error-reply code says should never happen (and bitcoin-cli always sends 2.0 requests), and a batch with one denied method is rejected as a whole with no indication of which method was refused. Giving the denial a JSON-RPC error object, while keeping status 403 for HTTP clients, fixes the IPC symptom with no change to the IPC schema and gives HTTP clients a reason as well. The schema change in the report works, but it renames a field for everyone who builds against the schema and leaves the server's inconsistency in place.

Status

Present on master. Not a stability bug. Next step: a Bitcoin Core change that gives the whitelist denial a JSON-RPC error. No public issue or PR yet.

Reported privately in an independent security review on 2026-09-27.