mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add packet size validation to SMSG_ITEM_QUERY_SINGLE_RESPONSE parsing
Improve robustness of item query response parsing across all three expansions by adding defensive size checks and bounds validation: - WotLK (world_packets.cpp): Add upfront validation for fixed-size fields, bounds cap on statsCount (max 10), in-loop size checks for stat pairs, and improved logging for truncation detection - Classic (packet_parsers_classic.cpp): Add upfront validation for fixed fields, in-loop checks for 10 fixed stat pairs and 5 damage entries, and graceful truncation handling - TBC (packet_parsers_tbc.cpp): Add upfront validation, statsCount bounds cap, and in-loop size checks for variable-length stats and fixed damage entries All changes are backward compatible and log warnings on packet truncation. This is part of ongoing Tier 2 work to improve multi-expansion packet parsing robustness against malformed or truncated server packets.
This commit is contained in:
parent
d7e1a3773c
commit
f472ee3be8
3 changed files with 138 additions and 3 deletions
|
|
@ -1263,6 +1263,12 @@ network::Packet ClassicPacketParsers::buildItemQuery(uint32_t entry, uint64_t gu
|
|||
}
|
||||
|
||||
bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) {
|
||||
// Validate minimum packet size: entry(4)
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.entry = packet.readUInt32();
|
||||
|
||||
// High bit set means item not found
|
||||
|
|
@ -1271,6 +1277,12 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
return true;
|
||||
}
|
||||
|
||||
// Validate minimum size for fixed fields: itemClass(4) + subClass(4) + 4 name strings + displayInfoId(4) + quality(4)
|
||||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before names (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t itemClass = packet.readUInt32();
|
||||
uint32_t subClass = packet.readUInt32();
|
||||
// Vanilla: NO SoundOverrideSubclass
|
||||
|
|
@ -1319,6 +1331,12 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
data.displayInfoId = packet.readUInt32();
|
||||
data.quality = packet.readUInt32();
|
||||
|
||||
// Validate minimum size for fixed fields: Flags(4) + BuyPrice(4) + SellPrice(4) + inventoryType(4)
|
||||
if (packet.getSize() - packet.getReadPos() < 16) {
|
||||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before inventoryType (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // Flags
|
||||
// Vanilla: NO Flags2
|
||||
packet.readUInt32(); // BuyPrice
|
||||
|
|
@ -1326,6 +1344,12 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
|
||||
data.inventoryType = packet.readUInt32();
|
||||
|
||||
// Validate minimum size for remaining fixed fields: 13×4 = 52 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 52) {
|
||||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before stats (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // AllowableClass
|
||||
packet.readUInt32(); // AllowableRace
|
||||
data.itemLevel = packet.readUInt32();
|
||||
|
|
@ -1341,8 +1365,16 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
// Vanilla: 10 stat pairs, NO statsCount prefix
|
||||
// Vanilla: 10 stat pairs, NO statsCount prefix (10×8 = 80 bytes)
|
||||
if (packet.getSize() - packet.getReadPos() < 80) {
|
||||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated in stats section (entry=", data.entry, ")");
|
||||
// Read what we can
|
||||
}
|
||||
for (uint32_t i = 0; i < 10; i++) {
|
||||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")");
|
||||
break;
|
||||
}
|
||||
uint32_t statType = packet.readUInt32();
|
||||
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
||||
if (statType != 0) {
|
||||
|
|
@ -1365,6 +1397,11 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
// Vanilla: 5 damage types (same count as WotLK)
|
||||
bool haveWeaponDamage = false;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// Each damage entry is dmgMin(4) + dmgMax(4) + damageType(4) = 12 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 12) {
|
||||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: damage ", i, " truncated (entry=", data.entry, ")");
|
||||
break;
|
||||
}
|
||||
float dmgMin = packet.readFloat();
|
||||
float dmgMax = packet.readFloat();
|
||||
uint32_t damageType = packet.readUInt32();
|
||||
|
|
@ -1378,6 +1415,11 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ
|
|||
}
|
||||
}
|
||||
|
||||
// Validate minimum size for armor field (4 bytes)
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before armor (entry=", data.entry, ")");
|
||||
return true; // Have core fields; armor is important but optional
|
||||
}
|
||||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
|
||||
// Remaining tail can vary by core. Read resistances + delay when present.
|
||||
|
|
|
|||
|
|
@ -958,12 +958,24 @@ bool TbcPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQuery
|
|||
// - Has statsCount prefix (Classic reads 10 pairs with no prefix)
|
||||
// ============================================================================
|
||||
bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) {
|
||||
// Validate minimum packet size: entry(4)
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.entry = packet.readUInt32();
|
||||
if (data.entry & 0x80000000) {
|
||||
data.entry &= ~0x80000000;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate minimum size for fixed fields: itemClass(4) + subClass(4) + soundOverride(4) + 4 name strings + displayInfoId(4) + quality(4)
|
||||
if (packet.getSize() - packet.getReadPos() < 12) {
|
||||
LOG_ERROR("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before names (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t itemClass = packet.readUInt32();
|
||||
uint32_t subClass = packet.readUInt32();
|
||||
data.itemClass = itemClass;
|
||||
|
|
@ -980,6 +992,12 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
data.displayInfoId = packet.readUInt32();
|
||||
data.quality = packet.readUInt32();
|
||||
|
||||
// Validate minimum size for fixed fields: Flags(4) + BuyPrice(4) + SellPrice(4) + inventoryType(4)
|
||||
if (packet.getSize() - packet.getReadPos() < 16) {
|
||||
LOG_ERROR("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before inventoryType (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // Flags (TBC: 1 flags field only — no Flags2)
|
||||
// TBC: NO Flags2, NO BuyCount
|
||||
packet.readUInt32(); // BuyPrice
|
||||
|
|
@ -987,6 +1005,12 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
|
||||
data.inventoryType = packet.readUInt32();
|
||||
|
||||
// Validate minimum size for remaining fixed fields: 13×4 = 52 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 52) {
|
||||
LOG_ERROR("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before statsCount (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // AllowableClass
|
||||
packet.readUInt32(); // AllowableRace
|
||||
data.itemLevel = packet.readUInt32();
|
||||
|
|
@ -1003,9 +1027,22 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
// TBC: statsCount prefix + exactly statsCount pairs (WotLK always sends 10)
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated at statsCount (entry=", data.entry, ")");
|
||||
return true; // Have core fields; stats are optional
|
||||
}
|
||||
uint32_t statsCount = packet.readUInt32();
|
||||
if (statsCount > 10) statsCount = 10; // sanity cap
|
||||
if (statsCount > 10) {
|
||||
LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: statsCount=", statsCount, " exceeds max 10 (entry=",
|
||||
data.entry, "), capping");
|
||||
statsCount = 10;
|
||||
}
|
||||
for (uint32_t i = 0; i < statsCount; i++) {
|
||||
// Each stat is 2 uint32s = 8 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||||
LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")");
|
||||
break;
|
||||
}
|
||||
uint32_t statType = packet.readUInt32();
|
||||
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
||||
switch (statType) {
|
||||
|
|
@ -1022,9 +1059,14 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
}
|
||||
// TBC: NO ScalingStatDistribution, NO ScalingStatValue (WotLK-only)
|
||||
|
||||
// 5 damage entries
|
||||
// 5 damage entries (5×12 = 60 bytes)
|
||||
bool haveWeaponDamage = false;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// Each damage entry is dmgMin(4) + dmgMax(4) + damageType(4) = 12 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 12) {
|
||||
LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: damage ", i, " truncated (entry=", data.entry, ")");
|
||||
break;
|
||||
}
|
||||
float dmgMin = packet.readFloat();
|
||||
float dmgMax = packet.readFloat();
|
||||
uint32_t damageType = packet.readUInt32();
|
||||
|
|
@ -1037,6 +1079,11 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
|||
}
|
||||
}
|
||||
|
||||
// Validate minimum size for armor (4 bytes)
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before armor (entry=", data.entry, ")");
|
||||
return true; // Have core fields; armor is important but optional
|
||||
}
|
||||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
|
||||
if (packet.getSize() - packet.getReadPos() >= 28) {
|
||||
|
|
|
|||
|
|
@ -2429,6 +2429,12 @@ static const char* getItemSubclassName(uint32_t itemClass, uint32_t subClass) {
|
|||
}
|
||||
|
||||
bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseData& data) {
|
||||
// Validate minimum packet size: entry(4) + item not found check
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.entry = packet.readUInt32();
|
||||
|
||||
// High bit set means item not found
|
||||
|
|
@ -2438,6 +2444,13 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
return true;
|
||||
}
|
||||
|
||||
// Validate minimum size for fixed fields before reading: itemClass(4) + subClass(4) + soundOverride(4)
|
||||
// + 4 name strings + displayInfoId(4) + quality(4) = at least 24 bytes more
|
||||
if (packet.getSize() - packet.getReadPos() < 24) {
|
||||
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before displayInfoId (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t itemClass = packet.readUInt32();
|
||||
uint32_t subClass = packet.readUInt32();
|
||||
data.itemClass = itemClass;
|
||||
|
|
@ -2459,6 +2472,10 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
// Some server variants omit BuyCount (4 fields instead of 5).
|
||||
// Read 5 fields and validate InventoryType; if it looks implausible, rewind and try 4.
|
||||
const size_t postQualityPos = packet.getReadPos();
|
||||
if (packet.getSize() - packet.getReadPos() < 24) {
|
||||
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before flags (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
packet.readUInt32(); // Flags
|
||||
packet.readUInt32(); // Flags2
|
||||
packet.readUInt32(); // BuyCount
|
||||
|
|
@ -2476,6 +2493,11 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
data.inventoryType = packet.readUInt32();
|
||||
}
|
||||
|
||||
// Validate minimum size for remaining fixed fields before inventoryType through containerSlots: 13×4 = 52 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 52) {
|
||||
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before statsCount (entry=", data.entry, ")");
|
||||
return false;
|
||||
}
|
||||
packet.readUInt32(); // AllowableClass
|
||||
packet.readUInt32(); // AllowableRace
|
||||
data.itemLevel = packet.readUInt32();
|
||||
|
|
@ -2491,10 +2513,29 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
// Read statsCount with bounds validation
|
||||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated at statsCount (entry=", data.entry, ")");
|
||||
return true; // Have enough for core fields; stats are optional
|
||||
}
|
||||
uint32_t statsCount = packet.readUInt32();
|
||||
|
||||
// Cap statsCount to prevent excessive iteration
|
||||
constexpr uint32_t kMaxItemStats = 10;
|
||||
if (statsCount > kMaxItemStats) {
|
||||
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: statsCount=", statsCount, " exceeds max ",
|
||||
kMaxItemStats, " (entry=", data.entry, "), capping");
|
||||
statsCount = kMaxItemStats;
|
||||
}
|
||||
|
||||
// Server sends exactly statsCount stat pairs (not always 10).
|
||||
uint32_t statsToRead = std::min(statsCount, 10u);
|
||||
for (uint32_t i = 0; i < statsToRead; i++) {
|
||||
// Each stat is 2 uint32s (type + value) = 8 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||||
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")");
|
||||
break;
|
||||
}
|
||||
uint32_t statType = packet.readUInt32();
|
||||
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
||||
switch (statType) {
|
||||
|
|
@ -2510,6 +2551,11 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
}
|
||||
}
|
||||
|
||||
// ScalingStatDistribution and ScalingStatValue
|
||||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||||
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before scaling stats (entry=", data.entry, ")");
|
||||
return true; // Have core fields; scaling is optional
|
||||
}
|
||||
packet.readUInt32(); // ScalingStatDistribution
|
||||
packet.readUInt32(); // ScalingStatValue
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue