Merge bitcoin/bitcoin#35582: rpc: reject null for optional parameters
What changed, and why it matters
This Bitcoin Core change tightens how three RPC commands (scantxoutset, scanblocks, deriveaddresses) handle the value null when it is passed for optional parameters. Previously, explicitly passing null could be treated differently from simply omitting the parameter, which could lead to unexpected behavior or errors. The patch makes null equivalent to 'not provided' for these specific cases and adds tests to confirm the new behavior. There is no direct evidence in the commit of a security vulnerability being exploited, but the change removes a potential source of confusion and misuse.
Review the RPC parameter handling changes for correctness and ensure that all optional-but-contextually-required parameters across the RPC interface are consistently handled. No immediate security response appears necessary, but operators should upgrade to include this hardening once it is released.
Security signals we found
RPC parameter validation change
Null value handling change
Addition of explicit error checks for missing required contextual parameters
Functional test coverage added for null parameter behavior
Evidence from the diff
The commit modifies RPC parameter handling in src/rpc/blockchain.cpp and src/rpc/output_script.cpp to use self.MaybeArg
Changed components
src/rpc/blockchain.cpp (scantxoutset, scanblocks RPC methods)src/rpc/output_script.cpp (deriveaddresses RPC method)test/functional/rpc_deriveaddresses.pytest/functional/rpc_scanblocks.pytest/functional/rpc_scantxoutset.pyInspect captured patch +18 / −7
### src/rpc/blockchain.cpp
@@ -2408,7 +2408,8 @@ static RPCMethod scantxoutset()
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
}
- if (request.params.size() < 2) {
+ const UniValue* scanobjects = self.MaybeArg<UniValue>("scanobjects");
+ if (!scanobjects) {
throw JSONRPCError(RPC_MISC_ERROR, "scanobjects argument is required for the start action");
}
@@ -2417,7 +2418,7 @@ static RPCMethod scantxoutset()
CAmount total_in = 0;
// loop through the scan objects
- for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
+ for (const UniValue& scanobject : scanobjects->get_array().getValues()) {
FlatSigningProvider provider;
auto scripts = EvalDescriptorStringOrObject(scanobject, provider);
for (CScript& script : scripts) {
@@ -2604,6 +2605,10 @@ static RPCMethod scanblocks()
if (!reserver.reserve()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
}
+ const UniValue* scanobjects = self.MaybeArg<UniValue>("scanobjects");
+ if (!scanobjects) {
+ throw JSONRPCError(RPC_MISC_ERROR, "scanobjects argument is required for the start action");
+ }
auto filtertype_name{self.Arg<std::string_view>("filtertype")};
BlockFilterType filtertype;
@@ -2648,7 +2653,7 @@ static RPCMethod scanblocks()
// loop through the scan objects, add scripts to the needle_set
GCSFilter::ElementSet needle_set;
- for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
+ for (const UniValue& scanobject : scanobjects->get_array().getValues()) {
FlatSigningProvider provider;
std::vector<CScript> scripts = EvalDescriptorStringOrObject(scanobject, provider);
for (const CScript& script : scripts) {
### src/rpc/output_script.cpp
@@ -306,8 +306,9 @@ static RPCMethod deriveaddresses()
int64_t range_begin = 0;
int64_t range_end = 0;
- if (request.params.size() >= 2 && !request.params[1].isNull()) {
- std::tie(range_begin, range_end) = ParseDescriptorRange(request.params[1]);
+ const UniValue* range = self.MaybeArg<UniValue>("range");
+ if (range) {
+ std::tie(range_begin, range_end) = ParseDescriptorRange(*range);
}
FlatSigningProvider key_provider;
@@ -317,11 +318,11 @@ static RPCMethod deriveaddresses()
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
}
auto& desc = descs.at(0);
- if (!desc->IsRange() && request.params.size() > 1) {
+ if (!desc->IsRange() && range) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
}
- if (desc->IsRange() && request.params.size() == 1) {
+ if (desc->IsRange() && !range) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range must be specified for a ranged descriptor");
}
### test/functional/rpc_deriveaddresses.py
@@ -35,6 +35,7 @@ def run_test(self):
assert_raises_rpc_error(-8, "Range should not be specified for an un-ranged descriptor", self.nodes[0].deriveaddresses, descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"), [0, 2])
assert_raises_rpc_error(-8, "Range must be specified for a ranged descriptor", self.nodes[0].deriveaddresses, descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)"))
+ assert_raises_rpc_error(-8, "Range must be specified for a ranged descriptor", self.nodes[0].deriveaddresses, descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)"), None)
assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].deriveaddresses, descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)"), 10000000000)
### test/functional/rpc_scanblocks.py
@@ -134,6 +134,9 @@ def run_test(self):
# test invalid command
assert_raises_rpc_error(-8, "Invalid action 'foobar'", node.scanblocks, "foobar")
+ # test that null scanobjects is rejected for start
+ assert_raises_rpc_error(-1, "scanobjects argument is required for the start action", node.scanblocks, "start", None)
+
if __name__ == '__main__':
ScanblocksTest(__file__).main()
### test/functional/rpc_scantxoutset.py
@@ -134,6 +134,7 @@ def run_test(self):
# Check that second arg is needed for start
assert_raises_rpc_error(-1, "scanobjects argument is required for the start action", self.nodes[0].scantxoutset, "start")
+ assert_raises_rpc_error(-1, "scanobjects argument is required for the start action", self.nodes[0].scantxoutset, "start", None)
# Check that invalid command give error
assert_raises_rpc_error(-8, "Invalid action 'invalid_command'", self.nodes[0].scantxoutset, "invalid_command")Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.