rpc: reject null for optional parameters
What changed, and why it matters
This Bitcoin Core change tightens how three RPC commands—deriveaddresses, scanblocks, and scantxoutset—handle the JSON value null when it is passed for an optional parameter. Previously, passing null could be treated the same as not passing the argument at all, which in some cases caused the command to proceed with missing required data and either crash or behave unexpectedly. The patch makes the RPC framework explicitly reject null for these parameters, returning a clear error instead. It is a defensive hardening fix rather than a fix for an active exploit.
Treat as a low-severity hardening patch. Review whether other RPCs using MaybeArg or size-based optional-parameter checks have similar null-handling gaps. No urgent deployment action is indicated, but include in normal release testing.
Security signals we found
Null-value handling in RPC parameter parsing
Potential null-pointer / invalid-array-access in scanblocks and scantxoutset
Incorrect error path for ranged descriptors in deriveaddresses when null range supplied
Addition of negative functional tests for null parameter rejection
Evidence from the diff
The commit replaces size-based parameter checks with self.MaybeArg
Changed components
src/rpc/blockchain.cpp (scantxoutset, scanblocks RPCs)src/rpc/output_script.cpp (deriveaddresses RPC)test/functional/rpc_deriveaddresses.pytest/functional/rpc_scanblocks.pytest/functional/rpc_scantxoutset.pyInspect captured patch +18 / −7
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index d78b82bc..d8cb77a7 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -2413,7 +2413,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");
}
@@ -2422,7 +2423,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) {
@@ -2609,6 +2610,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;
@@ -2653,7 +2658,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) {
diff --git a/src/rpc/output_script.cpp b/src/rpc/output_script.cpp
index cb73fe54..3110b70d 100644
--- a/src/rpc/output_script.cpp
+++ b/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");
}
diff --git a/test/functional/rpc_deriveaddresses.py b/test/functional/rpc_deriveaddresses.py
index f704223f..6fc79418 100755
--- a/test/functional/rpc_deriveaddresses.py
+++ b/test/functional/rpc_deriveaddresses.py
@@ -35,6 +35,7 @@ class DeriveaddressesTest(BitcoinTestFramework):
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)
diff --git a/test/functional/rpc_scanblocks.py b/test/functional/rpc_scanblocks.py
index 0b13741e..c10051d0 100755
--- a/test/functional/rpc_scanblocks.py
+++ b/test/functional/rpc_scanblocks.py
@@ -134,6 +134,9 @@ class ScanblocksTest(BitcoinTestFramework):
# 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()
diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py
index d59bd31a..a07ee735 100755
--- a/test/functional/rpc_scantxoutset.py
+++ b/test/functional/rpc_scantxoutset.py
@@ -134,6 +134,7 @@ class ScantxoutsetTest(BitcoinTestFramework):
# 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.