fix: Require vault_id and owner to be strings

asString() throws on an object or an array, so a request carrying either came back as an internal
error instead of a diagnosis. Both fields now have to be strings, which is what ledger_entry already
requires of the same fields, and a JSON number is no longer read as its decimal text.

The upper bound on seq goes in the same pass: isInt() or isUInt() already caps the value at 32 bits,
so the comparison against kMaxUInt could never fire.
This commit is contained in:
Timur Ialymov
2026-08-18 18:55:37 +01:00
parent 0833d8825a
commit c91ae2c918
3 changed files with 57 additions and 11 deletions

View File

@@ -56,6 +56,7 @@ This section contains changes targeting a future version.
- Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529)
- `vault_info`: Errors now identify what the request got wrong instead of reporting every failure as the unregistered token `malformedRequest`, and the `error`, `error_code` and `error_message` fields now agree with each other. An invalid `vault_id` or `seq` returns `invalidParams`, an invalid `owner` returns `actMalformed`, and a request that mixes `vault_id` with `owner`/`seq` or supplies neither returns `invalidParams` with a message naming the accepted combinations. [#8015](https://github.com/XRPLF/rippled/pull/8015)
- `vault_info`: A well-formed all-zero `vault_id` now returns `entryNotFound` instead of being rejected as malformed, and `entryNotFound` responses now include `error_code` and `error_message`. Clients that request `ripplerpc` 3.0 or above therefore receive HTTP 400 with that error rather than HTTP 200. [#8015](https://github.com/XRPLF/rippled/pull/8015)
- `vault_info`: `vault_id` and `owner` must now be strings, matching how `ledger_entry` reads the same fields. An object or an array in either field previously produced an internal error, and a number was silently converted to its decimal text; `vault_id` now returns `invalidParams` and `owner` returns `actMalformed`. [#8015](https://github.com/XRPLF/rippled/pull/8015)
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)

View File

@@ -5464,21 +5464,47 @@ class Vault_test : public beast::unit_test::Suite
}
{
testcase("RPC vault_info json zero vault_id");
testcase("RPC vault_info json numeric vault_id");
json::Value jvParams;
jvParams[jss::ledger_index] = jss::validated;
jvParams[jss::vault_id] = 0;
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
checkError(
jv[jss::result],
"invalidParams",
RpcInvalidParams,
"Invalid field 'vault_id', not hex string.");
}
{
testcase("RPC vault_info json object vault_id");
json::Value jvParams;
jvParams[jss::ledger_index] = jss::validated;
jvParams[jss::vault_id] = json::Value(json::ValueType::Object);
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
checkError(
jv[jss::result],
"invalidParams",
RpcInvalidParams,
"Invalid field 'vault_id', not hex string.");
}
{
// An all-zero key is a well-formed request for a vault that cannot exist, not a
// malformed one. parseHex accepts both the padded form and the short "0".
testcase("RPC vault_info json all zero vault_id");
json::Value jvParams;
jvParams[jss::ledger_index] = jss::validated;
jvParams[jss::vault_id] = strHex(uint256(beast::kZero));
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
checkError(jv[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
}
{
// An all-zero key is a well-formed request for a vault that cannot exist, not a
// malformed one.
testcase("RPC vault_info json all zero vault_id");
testcase("RPC vault_info json short zero vault_id");
json::Value jvParams;
jvParams[jss::ledger_index] = jss::validated;
jvParams[jss::vault_id] = strHex(uint256(beast::kZero));
jvParams[jss::vault_id] = "0";
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
checkError(jv[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
}
@@ -5560,6 +5586,20 @@ class Vault_test : public beast::unit_test::Suite
"Invalid field 'owner', not AccountID.");
}
{
testcase("RPC vault_info json array owner");
json::Value jvParams;
jvParams[jss::ledger_index] = jss::validated;
jvParams[jss::owner] = json::Value(json::ValueType::Array);
jvParams[jss::seq] = sequence;
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
checkError(
jv[jss::result],
"actMalformed",
RpcActMalformed,
"Invalid field 'owner', not AccountID.");
}
{
testcase("RPC vault_info json invalid combination only owner");
json::Value jvParams;

View File

@@ -26,7 +26,9 @@ parseVault(json::Value const& params, json::Value& jvResult)
uint256 uNodeIndex = beast::kZero;
if (hasVaultId && !hasOwner && !hasSeq)
{
if (!uNodeIndex.parseHex(params[jss::vault_id].asString()))
// asString() throws on an object or an array, so the type comes first.
auto const& vaultId = params[jss::vault_id];
if (!vaultId.isString() || !uNodeIndex.parseHex(vaultId.asString()))
{
rpc::injectError(
RpcInvalidParams, rpc::expectedFieldMessage(jss::vault_id, "hex string"), jvResult);
@@ -36,16 +38,19 @@ parseVault(json::Value const& params, json::Value& jvResult)
}
else if (!hasVaultId && hasOwner && hasSeq)
{
auto const id = parseBase58<AccountID>(params[jss::owner].asString());
auto const& owner = params[jss::owner];
auto const id = owner.isString() ? parseBase58<AccountID>(owner.asString())
: std::optional<AccountID>{};
if (!id)
{
rpc::injectError(
RpcActMalformed, rpc::expectedFieldMessage(jss::owner, "AccountID"), jvResult);
return std::nullopt;
}
if (!(params[jss::seq].isInt() || params[jss::seq].isUInt()) ||
params[jss::seq].asDouble() <= 0.0 ||
params[jss::seq].asDouble() > double(json::Value::kMaxUInt))
// Int and UInt are both 32 bits wide, so the type check is the only upper bound needed.
auto const& seqField = params[jss::seq];
if (!(seqField.isInt() || seqField.isUInt()) || seqField.asDouble() <= 0.0)
{
rpc::injectError(
RpcInvalidParams,
@@ -54,7 +59,7 @@ parseVault(json::Value const& params, json::Value& jvResult)
return std::nullopt;
}
auto const seq = SeqProxy::rawSequence(params[jss::seq].asUInt());
auto const seq = SeqProxy::rawSequence(seqField.asUInt());
uNodeIndex = keylet::vault(*id, seq).key;
}
else