diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 018e8af3..c96b06e1 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -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(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(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(packet.readUInt32()); // Remaining tail can vary by core. Read resistances + delay when present. diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 7c53bdfe..767a49ee 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -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(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(packet.readUInt32()); if (packet.getSize() - packet.getReadPos() >= 28) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 5d3989c7..60642168 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -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(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(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