Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rest: Reject truncated hex txid early in getutxos parsing #30482

Merged
merged 7 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/release-notes-30482.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Updated REST APIs
-----------------
- Parameter validation for `/rest/getutxos` has been improved by rejecting
truncated or overly large txids and malformed outpoint indices by raising an
HTTP_BAD_REQUEST "Parse error". Previously, these malformed requests would be
silently handled. (#30482, #30444)
12 changes: 6 additions & 6 deletions src/bitcoin-tx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ static void MutateTxAddInput(CMutableTransaction& tx, const std::string& strInpu
throw std::runtime_error("TX input missing separator");

// extract and validate TXID
uint256 txid;
if (!ParseHashStr(vStrInputParts[0], txid)) {
auto txid{Txid::FromHex(vStrInputParts[0])};
if (!txid) {
throw std::runtime_error("invalid TX input txid");
}

Expand All @@ -285,7 +285,7 @@ static void MutateTxAddInput(CMutableTransaction& tx, const std::string& strInpu
}

// append to transaction input list
CTxIn txin(Txid::FromUint256(txid), vout, CScript(), nSequenceIn);
CTxIn txin(*txid, vout, CScript(), nSequenceIn);
maflcko marked this conversation as resolved.
Show resolved Hide resolved
tx.vin.push_back(txin);
}

Expand Down Expand Up @@ -625,16 +625,16 @@ static void MutateTxSign(CMutableTransaction& tx, const std::string& flagStr)
if (!prevOut.checkObject(types))
throw std::runtime_error("prevtxs internal object typecheck fail");

uint256 txid;
if (!ParseHashStr(prevOut["txid"].get_str(), txid)) {
auto txid{Txid::FromHex(prevOut["txid"].get_str())};
if (!txid) {
throw std::runtime_error("txid must be hexadecimal string (not '" + prevOut["txid"].get_str() + "')");
}

const int nOut = prevOut["vout"].getInt<int>();
if (nOut < 0)
throw std::runtime_error("vout cannot be negative");

COutPoint out(Txid::FromUint256(txid), nOut);
COutPoint out(*txid, nOut);
std::vector<unsigned char> pkData(ParseHexUV(prevOut["scriptPubKey"], "scriptPubKey"));
CScript scriptPubKey(pkData.begin(), pkData.end());

Expand Down
9 changes: 0 additions & 9 deletions src/core_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,6 @@ std::string ScriptToAsmStr(const CScript& script, const bool fAttemptSighashDeco
[[nodiscard]] bool DecodeHexBlk(CBlock&, const std::string& strHexBlk);
bool DecodeHexBlockHeader(CBlockHeader&, const std::string& hex_header);

/**
* Parse a hex string into 256 bits
* @param[in] strHex a hex-formatted, 64-character string
* @param[out] result the result of the parsing
* @returns true if successful, false if not
*
* @see ParseHashV for an RPC-oriented version of this
*/
bool ParseHashStr(const std::string& strHex, uint256& result);
[[nodiscard]] util::Result<int> SighashFromStr(const std::string& sighash);

// core_write.cpp
Expand Down
9 changes: 0 additions & 9 deletions src/core_read.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,6 @@ bool DecodeHexBlk(CBlock& block, const std::string& strHexBlk)
return true;
}

bool ParseHashStr(const std::string& strHex, uint256& result)
{
if ((strHex.size() != 64) || !IsHex(strHex))
return false;

result.SetHex(strHex);
return true;
}

util::Result<int> SighashFromStr(const std::string& sighash)
{
static const std::map<std::string, int> map_sighash_values = {
Expand Down
2 changes: 1 addition & 1 deletion src/qt/transactiontablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ void TransactionTableModel::updateAmountColumnTitle()
void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction)
{
uint256 updated;
updated.SetHex(hash.toStdString());
updated.SetHexDeprecated(hash.toStdString());

priv->updateWallet(walletModel->wallet(), updated, status, showTransaction);
}
Expand Down
6 changes: 3 additions & 3 deletions src/qt/transactionview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ void TransactionView::contextualMenu(const QPoint &point)

// check if transaction can be abandoned, disable context menu action in case it doesn't
uint256 hash;
hash.SetHex(selection.at(0).data(TransactionTableModel::TxHashRole).toString().toStdString());
hash.SetHexDeprecated(selection.at(0).data(TransactionTableModel::TxHashRole).toString().toStdString());
abandonAction->setEnabled(model->wallet().transactionCanBeAbandoned(hash));
bumpFeeAction->setEnabled(model->wallet().transactionCanBeBumped(hash));
copyAddressAction->setEnabled(GUIUtil::hasEntryData(transactionView, 0, TransactionTableModel::AddressRole));
Expand All @@ -416,7 +416,7 @@ void TransactionView::abandonTx()
// get the hash from the TxHashRole (QVariant / QString)
uint256 hash;
QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString();
hash.SetHex(hashQStr.toStdString());
hash.SetHexDeprecated(hashQStr.toStdString());

// Abandon the wallet transaction over the walletModel
model->wallet().abandonTransaction(hash);
Expand All @@ -431,7 +431,7 @@ void TransactionView::bumpFee([[maybe_unused]] bool checked)
// get the hash from the TxHashRole (QVariant / QString)
uint256 hash;
QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString();
hash.SetHex(hashQStr.toStdString());
hash.SetHexDeprecated(hashQStr.toStdString());

// Bump tx fee over the walletModel
uint256 newHash;
Expand Down
44 changes: 24 additions & 20 deletions src/rest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,10 @@ static bool rest_headers(const std::any& context,
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
}

uint256 hash;
if (!ParseHashStr(hashStr, hash))
auto hash{uint256::FromHex(hashStr)};
if (!hash) {
maflcko marked this conversation as resolved.
Show resolved Hide resolved
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
}

const CBlockIndex* tip = nullptr;
std::vector<const CBlockIndex*> headers;
Expand All @@ -231,7 +232,7 @@ static bool rest_headers(const std::any& context,
LOCK(cs_main);
CChain& active_chain = chainman.ActiveChain();
tip = active_chain.Tip();
const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(hash);
const CBlockIndex* pindex{chainman.m_blockman.LookupBlockIndex(*hash)};
while (pindex != nullptr && active_chain.Contains(pindex)) {
headers.push_back(pindex);
if (headers.size() == *parsed_count) {
Expand Down Expand Up @@ -290,9 +291,10 @@ static bool rest_block(const std::any& context,
std::string hashStr;
const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);

uint256 hash;
if (!ParseHashStr(hashStr, hash))
auto hash{uint256::FromHex(hashStr)};
if (!hash) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
}

FlatFilePos pos{};
const CBlockIndex* pblockindex = nullptr;
Expand All @@ -303,7 +305,7 @@ static bool rest_block(const std::any& context,
{
LOCK(cs_main);
tip = chainman.ActiveChain().Tip();
pblockindex = chainman.m_blockman.LookupBlockIndex(hash);
pblockindex = chainman.m_blockman.LookupBlockIndex(*hash);
if (!pblockindex) {
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
}
Expand Down Expand Up @@ -390,8 +392,8 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
}

uint256 block_hash;
if (!ParseHashStr(raw_blockhash, block_hash)) {
auto block_hash{uint256::FromHex(raw_blockhash)};
if (!block_hash) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + raw_blockhash);
}

Expand All @@ -413,7 +415,7 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
ChainstateManager& chainman = *maybe_chainman;
LOCK(cs_main);
CChain& active_chain = chainman.ActiveChain();
const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(block_hash);
const CBlockIndex* pindex{chainman.m_blockman.LookupBlockIndex(*block_hash)};
while (pindex != nullptr && active_chain.Contains(pindex)) {
headers.push_back(pindex);
if (headers.size() == *parsed_count)
Expand Down Expand Up @@ -494,8 +496,8 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilter/<filtertype>/<blockhash>");
}

uint256 block_hash;
if (!ParseHashStr(uri_parts[1], block_hash)) {
auto block_hash{uint256::FromHex(uri_parts[1])};
if (!block_hash) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + uri_parts[1]);
}

Expand All @@ -516,7 +518,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
if (!maybe_chainman) return false;
ChainstateManager& chainman = *maybe_chainman;
LOCK(cs_main);
block_index = chainman.m_blockman.LookupBlockIndex(block_hash);
block_index = chainman.m_blockman.LookupBlockIndex(*block_hash);
if (!block_index) {
return RESTERR(req, HTTP_NOT_FOUND, uri_parts[1] + " not found");
}
Expand Down Expand Up @@ -616,14 +618,14 @@ static bool rest_deploymentinfo(const std::any& context, HTTPRequest* req, const
jsonRequest.params = UniValue(UniValue::VARR);

if (!hash_str.empty()) {
uint256 hash;
if (!ParseHashStr(hash_str, hash)) {
auto hash{uint256::FromHex(hash_str)};
if (!hash) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hash_str);
}

const ChainstateManager* chainman = GetChainman(context, req);
if (!chainman) return false;
if (!WITH_LOCK(::cs_main, return chainman->m_blockman.LookupBlockIndex(ParseHashV(hash_str, "blockhash")))) {
if (!WITH_LOCK(::cs_main, return chainman->m_blockman.LookupBlockIndex(*hash))) {
return RESTERR(req, HTTP_BAD_REQUEST, "Block not found");
}

Expand Down Expand Up @@ -704,9 +706,10 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
std::string hashStr;
const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);

uint256 hash;
if (!ParseHashStr(hashStr, hash))
auto hash{uint256::FromHex(hashStr)};
if (!hash) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
}

if (g_txindex) {
g_txindex->BlockUntilSyncedToCurrentChain();
Expand All @@ -715,7 +718,7 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
const NodeContext* const node = GetNodeContext(context, req);
if (!node) return false;
uint256 hashBlock = uint256();
const CTransactionRef tx = GetTransaction(/*block_index=*/nullptr, node->mempool.get(), hash, hashBlock, node->chainman->m_blockman);
const CTransactionRef tx{GetTransaction(/*block_index=*/nullptr, node->mempool.get(), *hash, hashBlock, node->chainman->m_blockman)};
if (!tx) {
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
}
Expand Down Expand Up @@ -792,13 +795,14 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
if (txid_out.size() != 2) {
return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
}
auto txid{Txid::FromHex(txid_out.at(0))};
auto output{ToIntegral<uint32_t>(txid_out.at(1))};

if (!output || !IsHex(txid_out.at(0))) {
if (!txid || !output) {
maflcko marked this conversation as resolved.
Show resolved Hide resolved
return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
maflcko marked this conversation as resolved.
Show resolved Hide resolved
}

vOutPoints.emplace_back(TxidFromString(txid_out.at(0)), *output);
vOutPoints.emplace_back(*txid, *output);
}

if (vOutPoints.size() > 0)
Expand Down
8 changes: 3 additions & 5 deletions src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,13 +345,11 @@ static RPCHelpMan generateblock()
std::vector<CTransactionRef> txs;
const auto raw_txs_or_txids = request.params[1].get_array();
for (size_t i = 0; i < raw_txs_or_txids.size(); i++) {
const auto str(raw_txs_or_txids[i].get_str());
const auto& str{raw_txs_or_txids[i].get_str()};

uint256 hash;
CMutableTransaction mtx;
if (ParseHashStr(str, hash)) {

const auto tx = mempool.get(hash);
if (auto hash{uint256::FromHex(str)}) {
const auto tx{mempool.get(*hash)};
if (!tx) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Transaction %s not in mempool.", str));
}
Expand Down
9 changes: 3 additions & 6 deletions src/test/blockfilter_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ BOOST_AUTO_TEST_CASE(blockfilters_json_test)

unsigned int pos = 0;
/*int block_height =*/ test[pos++].getInt<int>();
uint256 block_hash;
BOOST_CHECK(ParseHashStr(test[pos++].get_str(), block_hash));
BOOST_CHECK(uint256::FromHex(test[pos++].get_str()));

CBlock block;
BOOST_REQUIRE(DecodeHexBlk(block, test[pos++].get_str()));
Expand All @@ -165,11 +164,9 @@ BOOST_AUTO_TEST_CASE(blockfilters_json_test)
tx_undo.vprevout.emplace_back(txout, 0, false);
}

uint256 prev_filter_header_basic;
BOOST_CHECK(ParseHashStr(test[pos++].get_str(), prev_filter_header_basic));
uint256 prev_filter_header_basic{*Assert(uint256::FromHex(test[pos++].get_str()))};
maflcko marked this conversation as resolved.
Show resolved Hide resolved
std::vector<unsigned char> filter_basic = ParseHex(test[pos++].get_str());
maflcko marked this conversation as resolved.
Show resolved Hide resolved
uint256 filter_header_basic;
BOOST_CHECK(ParseHashStr(test[pos++].get_str(), filter_header_basic));
uint256 filter_header_basic{*Assert(uint256::FromHex(test[pos++].get_str()))};

BlockFilter computed_filter_basic(BlockFilterType::BASIC, block, block_undo);
BOOST_CHECK(computed_filter_basic.GetFilter().GetEncoded() == filter_basic);
Expand Down
3 changes: 1 addition & 2 deletions src/test/fuzz/hex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ FUZZ_TARGET(hex)
assert(ToLower(random_hex_string) == hex_data);
}
(void)IsHexNumber(random_hex_string);
uint256 result;
(void)ParseHashStr(random_hex_string, result);
(void)uint256::FromHex(random_hex_string);
(void)uint256S(random_hex_string);
try {
(void)HexToPubKey(random_hex_string);
Expand Down
6 changes: 3 additions & 3 deletions src/test/pow_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ BOOST_AUTO_TEST_CASE(CheckProofOfWork_test_negative_target)
uint256 hash;
unsigned int nBits;
nBits = UintToArith256(consensus.powLimit).GetCompact(true);
hash.SetHex("0x1");
hash = uint256{1};
maflcko marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@hodlinator hodlinator Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I was worried for a moment that this modification of switching to the uint8_t ctor was setting the other end of the base_blob array, but it's the same. Will be glad to see the fragile function go. 👍

BOOST_CHECK(!CheckProofOfWork(hash, nBits, consensus));
}

Expand All @@ -95,7 +95,7 @@ BOOST_AUTO_TEST_CASE(CheckProofOfWork_test_overflow_target)
const auto consensus = CreateChainParams(*m_node.args, ChainType::MAIN)->GetConsensus();
uint256 hash;
unsigned int nBits{~0x00800000U};
hash.SetHex("0x1");
hash = uint256{1};
BOOST_CHECK(!CheckProofOfWork(hash, nBits, consensus));
}

Expand All @@ -107,7 +107,7 @@ BOOST_AUTO_TEST_CASE(CheckProofOfWork_test_too_easy_target)
arith_uint256 nBits_arith = UintToArith256(consensus.powLimit);
nBits_arith *= 2;
nBits = nBits_arith.GetCompact();
hash.SetHex("0x1");
hash = uint256{1};
BOOST_CHECK(!CheckProofOfWork(hash, nBits, consensus));
}

Expand Down
18 changes: 9 additions & 9 deletions src/test/uint256_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ static std::string ArrayToString(const unsigned char A[], unsigned int width)
inline uint160 uint160S(std::string_view str)
{
uint160 rv;
rv.SetHex(str);
rv.SetHexDeprecated(str);
return rv;
}

Expand Down Expand Up @@ -157,7 +157,7 @@ BOOST_AUTO_TEST_CASE( comparison ) // <= >= < >
uint256S("1000000000000000000000000000000000000000000000000000000000000002"));
}

BOOST_AUTO_TEST_CASE( methods ) // GetHex SetHex begin() end() size() GetLow64 GetSerializeSize, Serialize, Unserialize
BOOST_AUTO_TEST_CASE(methods) // GetHex SetHexDeprecated FromHex begin() end() size() GetLow64 GetSerializeSize, Serialize, Unserialize
maflcko marked this conversation as resolved.
Show resolved Hide resolved
{
BOOST_CHECK_EQUAL(R1L.GetHex(), R1L.ToString());
BOOST_CHECK_EQUAL(R2L.GetHex(), R2L.ToString());
Expand All @@ -166,12 +166,12 @@ BOOST_AUTO_TEST_CASE( methods ) // GetHex SetHex begin() end() size() GetLow64 G
uint256 TmpL(R1L);
BOOST_CHECK_EQUAL(TmpL, R1L);
// Verify previous values don't persist when setting to truncated string.
TmpL.SetHex("21");
TmpL.SetHexDeprecated("21");
BOOST_CHECK_EQUAL(TmpL.ToString(), "0000000000000000000000000000000000000000000000000000000000000021");
TmpL.SetHex(R2L.ToString()); BOOST_CHECK_EQUAL(TmpL, R2L);
TmpL.SetHex(ZeroL.ToString()); BOOST_CHECK_EQUAL(TmpL, uint256());
BOOST_CHECK_EQUAL(uint256::FromHex(R2L.ToString()).value(), R2L);
BOOST_CHECK_EQUAL(uint256::FromHex(ZeroL.ToString()).value(), uint256());

TmpL.SetHex(R1L.ToString());
TmpL = uint256::FromHex(R1L.ToString()).value();
BOOST_CHECK_EQUAL_COLLECTIONS(R1L.begin(), R1L.end(), R1Array, R1Array + R1L.size());
BOOST_CHECK_EQUAL_COLLECTIONS(TmpL.begin(), TmpL.end(), R1Array, R1Array + TmpL.size());
BOOST_CHECK_EQUAL_COLLECTIONS(R2L.begin(), R2L.end(), R2Array, R2Array + R2L.size());
Expand Down Expand Up @@ -214,10 +214,10 @@ BOOST_AUTO_TEST_CASE( methods ) // GetHex SetHex begin() end() size() GetLow64 G
BOOST_CHECK_EQUAL(MaxS.GetHex(), MaxS.ToString());
uint160 TmpS(R1S);
BOOST_CHECK_EQUAL(TmpS, R1S);
TmpS.SetHex(R2S.ToString()); BOOST_CHECK_EQUAL(TmpS, R2S);
TmpS.SetHex(ZeroS.ToString()); BOOST_CHECK_EQUAL(TmpS, uint160());
BOOST_CHECK_EQUAL(uint160::FromHex(R2S.ToString()).value(), R2S);
BOOST_CHECK_EQUAL(uint160::FromHex(ZeroS.ToString()).value(), uint160());

TmpS.SetHex(R1S.ToString());
TmpS = uint160::FromHex(R1S.ToString()).value();
BOOST_CHECK_EQUAL_COLLECTIONS(R1S.begin(), R1S.end(), R1Array, R1Array + R1S.size());
BOOST_CHECK_EQUAL_COLLECTIONS(TmpS.begin(), TmpS.end(), R1Array, R1Array + TmpS.size());
BOOST_CHECK_EQUAL_COLLECTIONS(R2S.begin(), R2S.end(), R2Array, R2Array + R2S.size());
Expand Down
Loading
Loading