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:
Kelsi 2026-03-11 14:08:59 -07:00
parent d7e1a3773c
commit f472ee3be8
3 changed files with 138 additions and 3 deletions

View file

@ -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.