fix: Reject oversized validator manifest before decoding

This commit is contained in:
Bart
2026-07-16 16:21:56 -04:00
committed by Ed Hennis
parent 32a9cc4038
commit 0cce5a06d9
5 changed files with 88 additions and 21 deletions

View File

@@ -39,6 +39,35 @@
namespace xrpl {
namespace base64 {
/**
* Returns the maximum number of characters needed to base64-encode @p nBytes bytes.
*
* @param nBytes Number of input bytes.
* @return Size of the encoded string, including padding.
*/
constexpr std::size_t
encodedSize(std::size_t const nBytes)
{
return 4 * ((nBytes + 2) / 3);
}
/**
* Returns the maximum number of bytes a base64 string of @p nChars characters
* decodes to.
*
* @param nChars Number of base64 characters.
* @return Upper bound on the number of decoded bytes.
*/
constexpr std::size_t
decodedSize(std::size_t const nChars)
{
return ((nChars / 4) * 3) + 2;
}
} // namespace base64
std::string
base64Encode(std::uint8_t const* data, std::size_t len);

View File

@@ -1,10 +1,16 @@
#pragma once
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base64.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>
#include <shared_mutex>
#include <string>
@@ -135,15 +141,47 @@ struct Manifest
std::string
to_string(Manifest const& m);
/** Constructs Manifest from serialized string
/**
*Largest a valid manifest can be, in decoded bytes.
*
* A manifest has a fixed set of fields. Each is serialized as a field header
* (1-2 bytes), an optional length prefix (1 byte for these sizes), and the
* field body. Taking every field at its largest gives the maximum below, so
* anything larger cannot be a valid manifest.
*
* Field header + length + body = bytes
* sfVersion (U16) 2 0 2 4
* sfSequence (U32) 1 0 4 5
* sfPublicKey (33) 1 1 33 35
* sfSigningPubKey (33) 1 1 33 35
* sfSignature (72) 1 1 72 74
* sfMasterSignature (72) 2 1 72 75
* sfDomain (128) 1 1 128 130
* -----
* 358
*/
constexpr std::size_t kMaxManifestBytes = 358;
@param s Serialized manifest string
/**
* Largest a valid manifest can be, in base64 characters.
*
* base64 encodes 3 bytes as 4 characters, so this is the encoded form of
* @ref kMaxManifestBytes. Callers that receive a base64 manifest should
* reject anything longer than this before decoding, to avoid allocating
* memory for an oversized input.
*/
constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes);
@return `std::nullopt` if string is invalid
@note This does not verify manifest signatures.
`Manifest::verify` should be called after constructing manifest.
*/
/**
* Constructs Manifest from serialized string
*
* @param s Serialized manifest string
*
* @return `std::nullopt` if string is invalid
*
* @note This does not verify manifest signatures.
* `Manifest::verify` should be called after constructing manifest.
*/
/** @{ */
std::optional<Manifest>
deserializeManifest(Slice s, beast::Journal journal);

View File

@@ -76,20 +76,6 @@ getInverse()
return &kTab[0];
}
/// Returns max chars needed to encode a base64 string
constexpr std::size_t
encodedSize(std::size_t n)
{
return 4 * ((n + 2) / 3);
}
/// Returns max bytes needed to decode a base64 string
constexpr std::size_t
decodedSize(std::size_t n)
{
return ((n / 4) * 3) + 2;
}
/** Encode a series of octets as a padded, base64 string.
The resulting string will not be null terminated.

View File

@@ -62,6 +62,11 @@ deserializeManifest(Slice s, beast::Journal journal)
if (s.empty())
return std::nullopt;
// A valid manifest has a fixed maximum size, so reject anything larger
// before parsing it.
if (s.size() > kMaxManifestBytes)
return std::nullopt;
static SOTemplate const kManifestFormat{
// A manifest must include:
// - the master public key

View File

@@ -1130,6 +1130,15 @@ ValidatorList::applyList(
json::Value list;
auto const& manifest = localManifest ? *localManifest : globalManifest;
// Reject an oversized manifest before decoding it, so we do not allocate
// memory for an input that cannot be a valid manifest. deserializeManifest
// also enforces the decoded-byte limit, but checking here avoids the
// base64 decode entirely.
if (manifest.size() > kMaxManifestBase64)
{
JLOG(j_.warn()) << "UNL manifest exceeds maximum size";
return PublisherListStats{ListDisposition::Invalid};
}
auto m = deserializeManifest(base64Decode(manifest));
if (!m)
{