Compare commits

...

31 commits

Author SHA1 Message Date
Kelsi
2aebf3dd2f fix: reject malformed monster move payloads 2026-03-14 21:49:48 -07:00
Kelsi
98acf28bee fix(combatlog): reject truncated spell damage log tails 2026-03-14 21:49:48 -07:00
Kelsi
73f38eaa76 fix(combatlog): reject truncated resist logs 2026-03-14 21:49:48 -07:00
Kelsi
a42428d117 fix(combatlog): accept extended TBC spell damage payloads 2026-03-14 21:49:48 -07:00
Kelsi
f930ecbffd fix(combatlog): reject truncated instakill logs without spell id 2026-03-14 21:49:48 -07:00
Kelsi
c7dffccb4e fix(combatlog): reject spell start packets missing target flags 2026-03-14 21:49:48 -07:00
Kelsi
442d3baea5 fix(combatlog): reject truncated classic attacker-state packets 2026-03-14 21:49:48 -07:00
Kelsi
fb7a7bf76e fix(combatlog): enforce TBC attacker-state packet bounds 2026-03-14 21:49:48 -07:00
Kelsi
b2f03b2fe0 fix(combatlog): clamp attacker-state subdamage count to payload 2026-03-14 21:49:48 -07:00
Kelsi
87359f0e1c fix(combatlog): validate packed GUID bounds in spell energize log 2026-03-14 21:49:48 -07:00
Kelsi
4f5c051199 fix(combatlog): relax packed GUID minimum-size gates 2026-03-14 21:49:48 -07:00
Kelsi
b8cf867814 fix(combatlog): enforce TBC spell damage/heal packet bounds 2026-03-14 21:49:48 -07:00
Kelsi
51ef28e3f7 fix(combatlog): validate packed GUID bounds in spell damage/heal logs 2026-03-14 21:49:48 -07:00
Kelsi
96fc315c47 fix(combatlog): reject truncated spell start target GUIDs 2026-03-14 21:49:48 -07:00
Kelsi
fbcbdc2935 fix(combatlog): reject truncated spell go packets missing counts 2026-03-14 21:49:48 -07:00
Kelsi
4e97a19b23 fix(combatlog): avoid partial spell miss log entries on truncation 2026-03-14 21:49:48 -07:00
Kelsi
3dba13bbd2 fix(combatlog): reset spell go parser output before decode 2026-03-14 21:49:48 -07:00
Kelsi
debc47b5cc fix(combatlog): fail classic and tbc spell go parse on truncation 2026-03-14 21:49:48 -07:00
Kelsi
2006b71d48 fix(combatlog): enforce full spell start fixed-field bounds 2026-03-14 21:49:48 -07:00
Kelsi
0c7dfdebe9 fix(combatlog): fail spell go parse on truncated target lists 2026-03-14 21:49:48 -07:00
Kelsi
43cc2635ac fix(combatlog): validate packed GUID bounds in attacker state parsers 2026-03-14 21:49:48 -07:00
Kelsi
98267d6517 fix(combatlog): validate packed guid bounds in spell go parser 2026-03-14 21:49:48 -07:00
Kelsi
c32dbb082d fix(combatlog): validate packed GUID bounds in spell start parser 2026-03-14 21:49:48 -07:00
Kelsi
f2204f9d7b fix(combatlog): validate packed guid bounds in classic spell cast parsers 2026-03-14 21:49:48 -07:00
Kelsi
bfe9167a42 fix(combatlog): validate packed GUID bounds in spell cast parsers 2026-03-14 21:49:48 -07:00
Kelsi
0077986a22 fix(combatlog): consume full spell go target lists when capped 2026-03-14 21:49:48 -07:00
Kelsi
b8d694d6b3 fix(combatlog): parse full spell miss target lists 2026-03-14 21:49:48 -07:00
Kelsi
51cff764a9 fix(combatlog): map alternate immune2 spell miss value 2026-03-14 21:49:48 -07:00
Kelsi
7f5dedd57e fix(combatlog): map immune2 spell miss results correctly 2026-03-14 21:49:48 -07:00
Kelsi
6d06da52be fix(combatlog): show resisted amount from resist log packets 2026-03-14 21:49:48 -07:00
Kelsi
5756826723 fix(combatlog): preserve unknown source for environmental entries 2026-03-14 21:49:48 -07:00
4 changed files with 507 additions and 210 deletions

View file

@ -116,6 +116,25 @@ bool hasFullPackedGuid(const network::Packet& packet) {
return packet.getSize() - packet.getReadPos() >= guidBytes;
}
CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) {
switch (missInfo) {
case 0: return CombatTextEntry::MISS;
case 1: return CombatTextEntry::DODGE;
case 2: return CombatTextEntry::PARRY;
case 3: return CombatTextEntry::BLOCK;
case 4: return CombatTextEntry::EVADE;
case 5: return CombatTextEntry::IMMUNE;
case 6: return CombatTextEntry::DEFLECT;
case 7: return CombatTextEntry::ABSORB;
case 8: return CombatTextEntry::RESIST;
case 9: // Some cores encode SPELL_MISS_IMMUNE2 as 9.
case 10: // Others encode SPELL_MISS_IMMUNE2 as 10.
return CombatTextEntry::IMMUNE;
case 11: return CombatTextEntry::REFLECT;
default: return CombatTextEntry::MISS;
}
}
std::string formatCopperAmount(uint32_t amount) {
uint32_t gold = amount / 10000;
uint32_t silver = (amount / 100) % 100;
@ -2741,39 +2760,56 @@ void GameHandler::handlePacket(network::Packet& packet) {
uint64_t casterGuid = readSpellMissGuid();
if (packet.getSize() - packet.getReadPos() < 5) break;
/*uint8_t unk =*/ packet.readUInt8();
uint32_t count = packet.readUInt32();
count = std::min(count, 32u);
for (uint32_t i = 0; i < count; ++i) {
const uint32_t rawCount = packet.readUInt32();
if (rawCount > 128) {
LOG_WARNING("SMSG_SPELLLOGMISS: miss count capped (requested=", rawCount, ")");
}
const uint32_t storedLimit = std::min<uint32_t>(rawCount, 128u);
struct SpellMissLogEntry {
uint64_t victimGuid = 0;
uint8_t missInfo = 0;
};
std::vector<SpellMissLogEntry> parsedMisses;
parsedMisses.reserve(storedLimit);
bool truncated = false;
for (uint32_t i = 0; i < rawCount; ++i) {
if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u)
|| (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
truncated = true;
break;
}
uint64_t victimGuid = readSpellMissGuid();
if (packet.getSize() - packet.getReadPos() < 1) break;
uint8_t missInfo = packet.readUInt8();
const uint64_t victimGuid = readSpellMissGuid();
if (packet.getSize() - packet.getReadPos() < 1) {
truncated = true;
break;
}
const uint8_t missInfo = packet.readUInt8();
// REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult
if (missInfo == 11) {
if (packet.getSize() - packet.getReadPos() >= 5) {
/*uint32_t reflectSpellId =*/ packet.readUInt32();
/*uint8_t reflectResult =*/ packet.readUInt8();
} else {
packet.setReadPos(packet.getSize());
truncated = true;
break;
}
}
static const CombatTextEntry::Type missTypes[] = {
CombatTextEntry::MISS, // 0=MISS
CombatTextEntry::DODGE, // 1=DODGE
CombatTextEntry::PARRY, // 2=PARRY
CombatTextEntry::BLOCK, // 3=BLOCK
CombatTextEntry::EVADE, // 4=EVADE
CombatTextEntry::IMMUNE, // 5=IMMUNE
CombatTextEntry::DEFLECT, // 6=DEFLECT
CombatTextEntry::ABSORB, // 7=ABSORB
CombatTextEntry::RESIST, // 8=RESIST
};
CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo]
: (missInfo == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS);
if (i < storedLimit) {
parsedMisses.push_back({victimGuid, missInfo});
}
}
if (truncated) {
packet.setReadPos(packet.getSize());
break;
}
for (const auto& miss : parsedMisses) {
const uint64_t victimGuid = miss.victimGuid;
const uint8_t missInfo = miss.missInfo;
CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(missInfo);
if (casterGuid == playerGuid) {
// We cast a spell and it missed the target
addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid);
@ -4090,11 +4126,13 @@ void GameHandler::handlePacket(network::Packet& packet) {
return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0;
return UpdateObjectParser::readPackedGuid(packet);
};
if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) {
if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)
|| (!energizeTbc && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = readEnergizeGuid();
if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) {
if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)
|| (!energizeTbc && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t casterGuid = readEnergizeGuid();
@ -6489,7 +6527,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
uint64_t ikVictim = ikUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0;
if (ik_rem() < 4) {
packet.setReadPos(packet.getSize()); break;
}
uint32_t ikSpell = packet.readUInt32();
// Show kill/death feedback for the local player
if (ikCaster == playerGuid) {
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim);
@ -7105,11 +7146,19 @@ void GameHandler::handlePacket(network::Packet& packet) {
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; }
uint32_t spellId = packet.readUInt32();
// Resist payload includes:
// float resistFactor + uint32 targetResistance + uint32 resistedValue.
// Require the full payload so truncated packets cannot synthesize
// zero-value resist events.
if (rl_rem() < 12) { packet.setReadPos(packet.getSize()); break; }
/*float resistFactor =*/ packet.readFloat();
/*uint32_t targetRes =*/ packet.readUInt32();
int32_t resistedAmount = static_cast<int32_t>(packet.readUInt32());
// Show RESIST when the player is involved on either side.
if (victimGuid == playerGuid) {
addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, attackerGuid, victimGuid);
} else if (attackerGuid == playerGuid) {
addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, attackerGuid, victimGuid);
if (resistedAmount > 0 && victimGuid == playerGuid) {
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid);
} else if (resistedAmount > 0 && attackerGuid == playerGuid) {
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid);
}
packet.setReadPos(packet.getSize());
break;
@ -14381,8 +14430,11 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint
log.spellId = spellId;
log.isPlayerSource = isPlayerSource;
log.timestamp = std::time(nullptr);
// If the caller provided an explicit destination GUID but left source GUID as 0,
// preserve "unknown/no source" (e.g. environmental damage) instead of
// backfilling from current target.
uint64_t effectiveSrc = (srcGuid != 0) ? srcGuid
: (isPlayerSource ? playerGuid : targetGuid);
: ((dstGuid != 0) ? 0 : (isPlayerSource ? playerGuid : targetGuid));
uint64_t effectiveDst = (dstGuid != 0) ? dstGuid
: (isPlayerSource ? targetGuid : playerGuid);
log.sourceName = lookupName(effectiveSrc);
@ -17224,17 +17276,6 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
// Preserve spellId and actual participants for spell-go miss results.
// This keeps the persistent combat log aligned with the later GUID fixes.
if (!data.missTargets.empty()) {
static const CombatTextEntry::Type missTypes[] = {
CombatTextEntry::MISS, // 0=MISS
CombatTextEntry::DODGE, // 1=DODGE
CombatTextEntry::PARRY, // 2=PARRY
CombatTextEntry::BLOCK, // 3=BLOCK
CombatTextEntry::EVADE, // 4=EVADE
CombatTextEntry::IMMUNE, // 5=IMMUNE
CombatTextEntry::DEFLECT, // 6=DEFLECT
CombatTextEntry::ABSORB, // 7=ABSORB
CombatTextEntry::RESIST, // 8=RESIST
};
const uint64_t spellCasterGuid = data.casterUnit != 0 ? data.casterUnit : data.casterGuid;
const bool playerIsCaster = (spellCasterGuid == playerGuid);
@ -17242,8 +17283,7 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
if (!playerIsCaster && m.targetGuid != playerGuid) {
continue;
}
CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType]
: (m.missType == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS);
CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(m.missType);
addCombatText(ct, 0, data.spellId, playerIsCaster, 0, spellCasterGuid, m.targetGuid);
}
}

View file

@ -355,11 +355,21 @@ network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slo
// + uint16(targetFlags) [+ PackedGuid(unitTarget) if TARGET_FLAG_UNIT]
// ============================================================================
bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
data = SpellStartData{};
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
const size_t startPos = packet.getReadPos();
if (rem() < 2) return false;
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
if (rem() < 1) return false;
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
// uint8 castCount + uint32 spellId + uint16 castFlags + uint32 castTime = 11 bytes
@ -370,10 +380,18 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa
data.castTime = packet.readUInt32();
// SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK)
if (rem() < 2) return true;
if (rem() < 2) {
LOG_WARNING("[Classic] Spell start: missing targetFlags");
packet.setReadPos(startPos);
return false;
}
uint16_t targetFlags = packet.readUInt16();
// TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID
if (((targetFlags & 0x02) || (targetFlags & 0x800)) && rem() >= 1) {
if ((targetFlags & 0x02) || (targetFlags & 0x800)) {
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
}
@ -395,11 +413,16 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa
// + uint8(missCount) + [PackedGuid(missTarget) + uint8(missType)] × missCount
// ============================================================================
bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) {
// Always reset output to avoid stale targets when callers reuse buffers.
data = SpellGoData{};
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
const size_t startPos = packet.getReadPos();
if (rem() < 2) return false;
if (!hasFullPackedGuid(packet)) return false;
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
if (rem() < 1) return false;
if (!hasFullPackedGuid(packet)) return false;
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
// uint8 castCount + uint32 spellId + uint16 castFlags = 7 bytes
@ -408,52 +431,84 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
data.spellId = packet.readUInt32();
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
// Hit targets
if (rem() < 1) return true;
data.hitCount = packet.readUInt8();
// Cap hit count to prevent OOM from huge target lists
if (data.hitCount > 128) {
LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)data.hitCount, ")");
data.hitCount = 128;
// hitCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation.
if (rem() < 1) {
LOG_WARNING("[Classic] Spell go: missing hitCount after fixed fields");
packet.setReadPos(startPos);
return false;
}
data.hitTargets.reserve(data.hitCount);
for (uint8_t i = 0; i < data.hitCount && rem() >= 1; ++i) {
data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet));
const uint8_t rawHitCount = packet.readUInt8();
if (rawHitCount > 128) {
LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
}
// Check if we read all expected hits
if (data.hitTargets.size() < data.hitCount) {
LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(),
"/", (int)data.hitCount);
data.hitCount = data.hitTargets.size();
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
data.hitTargets.reserve(storedHitLimit);
bool truncatedTargets = false;
for (uint16_t i = 0; i < rawHitCount; ++i) {
if (!hasFullPackedGuid(packet)) {
LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", i,
"/", (int)rawHitCount);
truncatedTargets = true;
break;
}
const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet);
if (i < storedHitLimit) {
data.hitTargets.push_back(targetGuid);
}
}
if (truncatedTargets) {
packet.setReadPos(startPos);
return false;
}
data.hitCount = static_cast<uint8_t>(data.hitTargets.size());
// Miss targets
if (rem() < 1) return true;
data.missCount = packet.readUInt8();
// Cap miss count to prevent OOM
if (data.missCount > 128) {
LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)data.missCount, ")");
data.missCount = 128;
// missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation.
if (rem() < 1) {
LOG_WARNING("[Classic] Spell go: missing missCount after hit target list");
packet.setReadPos(startPos);
return false;
}
data.missTargets.reserve(data.missCount);
for (uint8_t i = 0; i < data.missCount && rem() >= 2; ++i) {
const uint8_t rawMissCount = packet.readUInt8();
if (rawMissCount > 128) {
LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")");
}
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
data.missTargets.reserve(storedMissLimit);
for (uint16_t i = 0; i < rawMissCount; ++i) {
if (!hasFullPackedGuid(packet)) {
LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", i,
"/", (int)rawMissCount);
truncatedTargets = true;
break;
}
SpellGoMissEntry m;
m.targetGuid = UpdateObjectParser::readPackedGuid(packet);
if (rem() < 1) break;
if (rem() < 1) {
LOG_WARNING("[Classic] Spell go: missing missType at miss index ", i,
"/", (int)rawMissCount);
truncatedTargets = true;
break;
}
m.missType = packet.readUInt8();
if (m.missType == 11) {
if (rem() < 5) break;
if (rem() < 5) {
LOG_WARNING("[Classic] Spell go: truncated reflect payload at miss index ", i,
"/", (int)rawMissCount);
truncatedTargets = true;
break;
}
(void)packet.readUInt32();
(void)packet.readUInt8();
}
data.missTargets.push_back(m);
if (i < storedMissLimit) {
data.missTargets.push_back(m);
}
}
// Check if we read all expected misses
if (data.missTargets.size() < data.missCount) {
LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", (int)data.missTargets.size(),
"/", (int)data.missCount);
data.missCount = data.missTargets.size();
if (truncatedTargets) {
packet.setReadPos(startPos);
return false;
}
data.missCount = static_cast<uint8_t>(data.missTargets.size());
LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
" misses=", (int)data.missCount);
@ -471,19 +526,42 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
// + uint32(victimState) + int32(overkill) [+ uint32(blocked)]
// ============================================================================
bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) {
data = AttackerStateUpdateData{};
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1)
const size_t startPos = packet.getReadPos();
data.hitInfo = packet.readUInt32();
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
if (rem() < 1) return false;
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
if (rem() < 5) return false; // int32 totalDamage + uint8 subDamageCount
if (rem() < 5) {
packet.setReadPos(startPos);
return false;
} // int32 totalDamage + uint8 subDamageCount
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
data.subDamageCount = packet.readUInt8();
for (uint8_t i = 0; i < data.subDamageCount && rem() >= 20; ++i) {
const uint8_t maxSubDamageCount = static_cast<uint8_t>(std::min<size_t>(rem() / 20, 64));
if (data.subDamageCount > maxSubDamageCount) {
data.subDamageCount = maxSubDamageCount;
}
data.subDamages.reserve(data.subDamageCount);
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
if (rem() < 20) {
packet.setReadPos(startPos);
return false;
}
SubDamage sub;
sub.schoolMask = packet.readUInt32();
sub.damage = packet.readFloat();
@ -492,8 +570,12 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att
sub.resisted = packet.readUInt32();
data.subDamages.push_back(sub);
}
data.subDamageCount = static_cast<uint8_t>(data.subDamages.size());
if (rem() < 8) return true;
if (rem() < 8) {
packet.setReadPos(startPos);
return false;
}
data.victimState = packet.readUInt32();
data.overkill = static_cast<int32_t>(packet.readUInt32());

View file

@ -1234,6 +1234,8 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector<MailMe
// Correct TBC format (cmangos-tbc): objectGuid(u64) + casterGuid(u64) + castCount(u8) + spellId(u32) + castFlags(u32) + castTime(u32)
// ============================================================================
bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
data = SpellStartData{};
const size_t startPos = packet.getReadPos();
if (packet.getSize() - packet.getReadPos() < 22) return false;
data.casterGuid = packet.readUInt64(); // full GUID (object)
@ -1243,11 +1245,20 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData&
data.castFlags = packet.readUInt32();
data.castTime = packet.readUInt32();
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t targetFlags = packet.readUInt32();
if ((targetFlags & 0x02) && packet.getReadPos() + 8 <= packet.getSize()) {
data.targetGuid = packet.readUInt64(); // full GUID in TBC
if (packet.getReadPos() + 4 > packet.getSize()) {
LOG_WARNING("[TBC] Spell start: missing targetFlags");
packet.setReadPos(startPos);
return false;
}
uint32_t targetFlags = packet.readUInt32();
const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT
if (needsTargetGuid) {
if (packet.getReadPos() + 8 > packet.getSize()) {
packet.setReadPos(startPos);
return false;
}
data.targetGuid = packet.readUInt64(); // full GUID in TBC
}
LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
@ -1261,6 +1272,10 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData&
// WotLK uses packed GUIDs and adds a timestamp (u32) after castFlags.
// ============================================================================
bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) {
// Always reset output to avoid stale targets when callers reuse buffers.
data = SpellGoData{};
const size_t startPos = packet.getReadPos();
// Fixed header before hit/miss lists:
// casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32)
if (packet.getSize() - packet.getReadPos() < 25) return false;
@ -1273,55 +1288,77 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data)
// NOTE: NO timestamp field here in TBC (WotLK added packet.readUInt32())
if (packet.getReadPos() >= packet.getSize()) {
LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " (no hit data)");
return true;
LOG_WARNING("[TBC] Spell go: missing hitCount after fixed fields");
packet.setReadPos(startPos);
return false;
}
data.hitCount = packet.readUInt8();
// Cap hit count to prevent OOM from huge target lists
if (data.hitCount > 128) {
LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)data.hitCount, ")");
data.hitCount = 128;
const uint8_t rawHitCount = packet.readUInt8();
if (rawHitCount > 128) {
LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
}
data.hitTargets.reserve(data.hitCount);
for (uint8_t i = 0; i < data.hitCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) {
data.hitTargets.push_back(packet.readUInt64()); // full GUID in TBC
}
// Check if we read all expected hits
if (data.hitTargets.size() < data.hitCount) {
LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(),
"/", (int)data.hitCount);
data.hitCount = data.hitTargets.size();
}
if (packet.getReadPos() < packet.getSize()) {
data.missCount = packet.readUInt8();
// Cap miss count to prevent OOM
if (data.missCount > 128) {
LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)data.missCount, ")");
data.missCount = 128;
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
data.hitTargets.reserve(storedHitLimit);
bool truncatedTargets = false;
for (uint16_t i = 0; i < rawHitCount; ++i) {
if (packet.getReadPos() + 8 > packet.getSize()) {
LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", i,
"/", (int)rawHitCount);
truncatedTargets = true;
break;
}
data.missTargets.reserve(data.missCount);
for (uint8_t i = 0; i < data.missCount && packet.getReadPos() + 9 <= packet.getSize(); ++i) {
SpellGoMissEntry m;
m.targetGuid = packet.readUInt64(); // full GUID in TBC
m.missType = packet.readUInt8();
if (m.missType == 11) {
if (packet.getReadPos() + 5 > packet.getSize()) {
break;
}
(void)packet.readUInt32();
(void)packet.readUInt8();
const uint64_t targetGuid = packet.readUInt64(); // full GUID in TBC
if (i < storedHitLimit) {
data.hitTargets.push_back(targetGuid);
}
}
if (truncatedTargets) {
packet.setReadPos(startPos);
return false;
}
data.hitCount = static_cast<uint8_t>(data.hitTargets.size());
if (packet.getReadPos() >= packet.getSize()) {
LOG_WARNING("[TBC] Spell go: missing missCount after hit target list");
packet.setReadPos(startPos);
return false;
}
const uint8_t rawMissCount = packet.readUInt8();
if (rawMissCount > 128) {
LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)rawMissCount, ")");
}
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
data.missTargets.reserve(storedMissLimit);
for (uint16_t i = 0; i < rawMissCount; ++i) {
if (packet.getReadPos() + 9 > packet.getSize()) {
LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", i,
"/", (int)rawMissCount);
truncatedTargets = true;
break;
}
SpellGoMissEntry m;
m.targetGuid = packet.readUInt64(); // full GUID in TBC
m.missType = packet.readUInt8();
if (m.missType == 11) {
if (packet.getReadPos() + 5 > packet.getSize()) {
LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i,
"/", (int)rawMissCount);
truncatedTargets = true;
break;
}
(void)packet.readUInt32();
(void)packet.readUInt8();
}
if (i < storedMissLimit) {
data.missTargets.push_back(m);
}
// Check if we read all expected misses
if (data.missTargets.size() < data.missCount) {
LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", (int)data.missTargets.size(),
"/", (int)data.missCount);
data.missCount = data.missTargets.size();
}
}
if (truncatedTargets) {
packet.setReadPos(startPos);
return false;
}
data.missCount = static_cast<uint8_t>(data.missTargets.size());
LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
" misses=", (int)data.missCount);
@ -1369,15 +1406,33 @@ bool TbcPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData&
// would mis-parse TBC's GUIDs and corrupt all subsequent damage fields.
// ============================================================================
bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) {
if (packet.getSize() - packet.getReadPos() < 21) return false;
data = AttackerStateUpdateData{};
data.hitInfo = packet.readUInt32();
data.attackerGuid = packet.readUInt64(); // full GUID in TBC
data.targetGuid = packet.readUInt64(); // full GUID in TBC
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
const size_t startPos = packet.getReadPos();
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
// Fixed fields before sub-damage list:
// hitInfo(4) + attackerGuid(8) + targetGuid(8) + totalDamage(4) + subDamageCount(1) = 25 bytes
if (rem() < 25) return false;
data.hitInfo = packet.readUInt32();
data.attackerGuid = packet.readUInt64(); // full GUID in TBC
data.targetGuid = packet.readUInt64(); // full GUID in TBC
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
data.subDamageCount = packet.readUInt8();
// Clamp to what can fit in the remaining payload (20 bytes per sub-damage entry).
const uint8_t maxSubDamageCount = static_cast<uint8_t>(std::min<size_t>(rem() / 20, 64));
if (data.subDamageCount > maxSubDamageCount) {
data.subDamageCount = maxSubDamageCount;
}
data.subDamages.reserve(data.subDamageCount);
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
if (rem() < 20) {
packet.setReadPos(startPos);
return false;
}
SubDamage sub;
sub.schoolMask = packet.readUInt32();
sub.damage = packet.readFloat();
@ -1387,10 +1442,17 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke
data.subDamages.push_back(sub);
}
data.subDamageCount = static_cast<uint8_t>(data.subDamages.size());
// victimState + overkill are part of the expected payload.
if (rem() < 8) {
packet.setReadPos(startPos);
return false;
}
data.victimState = packet.readUInt32();
data.overkill = static_cast<int32_t>(packet.readUInt32());
if (packet.getReadPos() < packet.getSize()) {
if (rem() >= 4) {
data.blocked = packet.readUInt32();
}
@ -1406,20 +1468,28 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke
// TBC uses full uint64 GUIDs; WotLK uses packed GUIDs.
// ============================================================================
bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) {
if (packet.getSize() - packet.getReadPos() < 29) return false;
// Fixed TBC payload size:
// targetGuid(8) + attackerGuid(8) + spellId(4) + damage(4) + schoolMask(1)
// + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4)
// = 43 bytes
// Some servers append additional trailing fields; consume the canonical minimum
// and leave any extension bytes unread.
if (packet.getSize() - packet.getReadPos() < 43) return false;
data.targetGuid = packet.readUInt64(); // full GUID in TBC
data = SpellDamageLogData{};
data.targetGuid = packet.readUInt64(); // full GUID in TBC
data.attackerGuid = packet.readUInt64(); // full GUID in TBC
data.spellId = packet.readUInt32();
data.damage = packet.readUInt32();
data.schoolMask = packet.readUInt8();
data.absorbed = packet.readUInt32();
data.resisted = packet.readUInt32();
data.spellId = packet.readUInt32();
data.damage = packet.readUInt32();
data.schoolMask = packet.readUInt8();
data.absorbed = packet.readUInt32();
data.resisted = packet.readUInt32();
uint8_t periodicLog = packet.readUInt8();
(void)periodicLog;
packet.readUInt8(); // unused
packet.readUInt32(); // blocked
packet.readUInt8(); // unused
packet.readUInt32(); // blocked
uint32_t flags = packet.readUInt32();
data.isCrit = (flags & 0x02) != 0;
@ -1437,13 +1507,17 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL
// TBC uses full uint64 GUIDs; WotLK uses packed GUIDs.
// ============================================================================
bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) {
if (packet.getSize() - packet.getReadPos() < 25) return false;
// Fixed payload is 28 bytes; many cores append crit flag (1 byte).
// targetGuid(8) + casterGuid(8) + spellId(4) + heal(4) + overheal(4)
if (packet.getSize() - packet.getReadPos() < 28) return false;
data.targetGuid = packet.readUInt64(); // full GUID in TBC
data.casterGuid = packet.readUInt64(); // full GUID in TBC
data.spellId = packet.readUInt32();
data.heal = packet.readUInt32();
data.overheal = packet.readUInt32();
data = SpellHealLogData{};
data.targetGuid = packet.readUInt64(); // full GUID in TBC
data.casterGuid = packet.readUInt64(); // full GUID in TBC
data.spellId = packet.readUInt32();
data.heal = packet.readUInt32();
data.overheal = packet.readUInt32();
// TBC has no absorbed field in SMSG_SPELLHEALLOG; skip crit flag
if (packet.getReadPos() < packet.getSize()) {
uint8_t critFlag = packet.readUInt8();

View file

@ -19,6 +19,22 @@ namespace {
inline uint16_t bswap16(uint16_t v) {
return static_cast<uint16_t>(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8));
}
bool hasFullPackedGuid(const wowee::network::Packet& packet) {
if (packet.getReadPos() >= packet.getSize()) {
return false;
}
const auto& rawData = packet.getData();
const uint8_t mask = rawData[packet.getReadPos()];
size_t guidBytes = 1;
for (int bit = 0; bit < 8; ++bit) {
if ((mask & (1u << bit)) != 0) {
++guidBytes;
}
}
return packet.getSize() - packet.getReadPos() >= guidBytes;
}
}
namespace wowee {
@ -3156,12 +3172,10 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
if (pointCount == 0) return true;
// Cap pointCount to prevent excessive iteration from malformed packets
// Reject extreme point counts from malformed packets.
constexpr uint32_t kMaxSplinePoints = 1000;
if (pointCount > kMaxSplinePoints) {
LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints,
" (guid=0x", std::hex, data.guid, std::dec, "), capping");
pointCount = kMaxSplinePoints;
return false;
}
// Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed).
@ -3169,20 +3183,27 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
if (uncompressed) {
const size_t requiredBytes = static_cast<size_t>(pointCount) * 12ull;
if (packet.getReadPos() + requiredBytes > packet.getSize()) return false;
// Read last point as destination
// Skip to last point: each point is 12 bytes
for (uint32_t i = 0; i < pointCount - 1; i++) {
if (packet.getReadPos() + 12 > packet.getSize()) return true;
if (packet.getReadPos() + 12 > packet.getSize()) return false;
packet.readFloat(); packet.readFloat(); packet.readFloat();
}
if (packet.getReadPos() + 12 > packet.getSize()) return true;
if (packet.getReadPos() + 12 > packet.getSize()) return false;
data.destX = packet.readFloat();
data.destY = packet.readFloat();
data.destZ = packet.readFloat();
data.hasDest = true;
} else {
// Compressed: first 3 floats are the destination (final point)
if (packet.getReadPos() + 12 > packet.getSize()) return true;
size_t requiredBytes = 12;
if (pointCount > 1) {
requiredBytes += static_cast<size_t>(pointCount - 1) * 4ull;
}
if (packet.getReadPos() + requiredBytes > packet.getSize()) return false;
data.destX = packet.readFloat();
data.destY = packet.readFloat();
data.destZ = packet.readFloat();
@ -3266,16 +3287,19 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d
if (pointCount == 0) return true;
// Cap pointCount to prevent excessive iteration from malformed packets
// Reject extreme point counts from malformed packets.
constexpr uint32_t kMaxSplinePoints = 1000;
if (pointCount > kMaxSplinePoints) {
LOG_WARNING("SMSG_MONSTER_MOVE(Vanilla): pointCount=", pointCount, " exceeds max ", kMaxSplinePoints,
" (guid=0x", std::hex, data.guid, std::dec, "), capping");
pointCount = kMaxSplinePoints;
return false;
}
size_t requiredBytes = 12;
if (pointCount > 1) {
requiredBytes += static_cast<size_t>(pointCount - 1) * 4ull;
}
if (packet.getReadPos() + requiredBytes > packet.getSize()) return false;
// First float[3] is destination.
if (packet.getReadPos() + 12 > packet.getSize()) return true;
data.destX = packet.readFloat();
data.destY = packet.readFloat();
data.destZ = packet.readFloat();
@ -3285,9 +3309,8 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d
if (pointCount > 1) {
size_t skipBytes = static_cast<size_t>(pointCount - 1) * 4;
size_t newPos = packet.getReadPos() + skipBytes;
if (newPos <= packet.getSize()) {
packet.setReadPos(newPos);
}
if (newPos > packet.getSize()) return false;
packet.setReadPos(newPos);
}
LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec,
@ -3327,7 +3350,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda
size_t startPos = packet.getReadPos();
data.hitInfo = packet.readUInt32();
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
// Validate totalDamage + subDamageCount can be read (5 bytes)
@ -3339,15 +3370,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
data.subDamageCount = packet.readUInt8();
// Cap subDamageCount: each entry is 20 bytes. If the claimed count
// Cap subDamageCount: each entry is 20 bytes. If the claimed count
// exceeds what the remaining bytes can hold, a GUID was mis-parsed
// (off by one byte), causing the school-mask byte to be read as count.
// In that case silently clamp to the number of full entries that fit.
// In that case clamp to the number of full entries that fit.
{
size_t remaining = packet.getSize() - packet.getReadPos();
size_t maxFit = remaining / 20;
if (data.subDamageCount > maxFit) {
data.subDamageCount = static_cast<uint8_t>(maxFit > 0 ? 1 : 0);
data.subDamageCount = static_cast<uint8_t>(std::min<size_t>(maxFit, 64));
} else if (data.subDamageCount > 64) {
data.subDamageCount = 64;
}
@ -3399,11 +3430,22 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda
}
bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) {
// Upfront validation: packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1) + absorbed(4) + resisted(4) = 30 bytes minimum
if (packet.getSize() - packet.getReadPos() < 30) return false;
// Upfront validation:
// packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1)
// + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4)
// = 33 bytes minimum.
if (packet.getSize() - packet.getReadPos() < 33) return false;
size_t startPos = packet.getReadPos();
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
// Validate core fields (spellId + damage + overkill + schoolMask + absorbed + resisted = 21 bytes)
@ -3419,11 +3461,11 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da
data.absorbed = packet.readUInt32();
data.resisted = packet.readUInt32();
// Skip remaining fields (periodicLog + unused + blocked + flags = 10 bytes)
// Remaining fields are required for a complete event.
// Reject truncated packets so we do not emit partial/incorrect combat entries.
if (packet.getSize() - packet.getReadPos() < 10) {
LOG_WARNING("SpellDamageLog: truncated trailing fields");
data.isCrit = false;
return true;
packet.setReadPos(startPos);
return false;
}
uint8_t periodicLog = packet.readUInt8();
@ -3445,7 +3487,15 @@ bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data)
if (packet.getSize() - packet.getReadPos() < 21) return false;
size_t startPos = packet.getReadPos();
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
// Validate remaining fields (spellId + heal + overheal + absorbed + critFlag = 17 bytes)
@ -3663,15 +3713,25 @@ bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) {
}
bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
// Upfront validation: packed GUID(1-8) + packed GUID(1-8) + castCount(1) + spellId(4) + castFlags(4) + castTime(4) = 22 bytes minimum
if (packet.getSize() - packet.getReadPos() < 22) return false;
data = SpellStartData{};
// Packed GUIDs are variable-length; only require minimal packet shape up front:
// two GUID masks + castCount(1) + spellId(4) + castFlags(4) + castTime(4).
if (packet.getSize() - packet.getReadPos() < 15) return false;
size_t startPos = packet.getReadPos();
if (!hasFullPackedGuid(packet)) {
return false;
}
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
// Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 9 bytes)
if (packet.getSize() - packet.getReadPos() < 9) {
// Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 13 bytes)
if (packet.getSize() - packet.getReadPos() < 13) {
packet.setReadPos(startPos);
return false;
}
@ -3681,12 +3741,21 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
data.castFlags = packet.readUInt32();
data.castTime = packet.readUInt32();
// Read target flags and target (simplified)
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t targetFlags = packet.readUInt32();
if ((targetFlags & 0x02) && packet.getSize() - packet.getReadPos() >= 1) { // TARGET_FLAG_UNIT, validate packed GUID read
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
// SpellCastTargets starts with target flags and is mandatory.
if (packet.getSize() - packet.getReadPos() < 4) {
LOG_WARNING("Spell start: missing targetFlags");
packet.setReadPos(startPos);
return false;
}
uint32_t targetFlags = packet.readUInt32();
const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT
if (needsTargetGuid) {
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
}
LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
@ -3694,12 +3763,22 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
}
bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
// Always reset output to avoid stale targets when callers reuse buffers.
data = SpellGoData{};
// Packed GUIDs are variable-length, so only require the smallest possible
// shape up front: 2 GUID masks + fixed fields through missCount.
if (packet.getSize() - packet.getReadPos() < 17) return false;
// shape up front: 2 GUID masks + fixed fields through hitCount.
if (packet.getSize() - packet.getReadPos() < 16) return false;
size_t startPos = packet.getReadPos();
if (!hasFullPackedGuid(packet)) {
return false;
}
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(startPos);
return false;
}
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
// Validate remaining fixed fields up to hitCount/missCount
@ -3714,59 +3793,81 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
// Timestamp in 3.3.5a
packet.readUInt32();
data.hitCount = packet.readUInt8();
// Cap hit count to prevent DoS via massive arrays
if (data.hitCount > 128) {
LOG_WARNING("Spell go: hitCount capped (requested=", (int)data.hitCount, ")");
data.hitCount = 128;
const uint8_t rawHitCount = packet.readUInt8();
if (rawHitCount > 128) {
LOG_WARNING("Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
}
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
data.hitTargets.reserve(data.hitCount);
for (uint8_t i = 0; i < data.hitCount; ++i) {
bool truncatedTargets = false;
data.hitTargets.reserve(storedHitLimit);
for (uint16_t i = 0; i < rawHitCount; ++i) {
// WotLK hit targets are packed GUIDs, like the caster and miss targets.
if (packet.getSize() - packet.getReadPos() < 1) {
LOG_WARNING("Spell go: truncated hit targets at index ", (int)i, "/", (int)data.hitCount);
data.hitCount = i;
if (!hasFullPackedGuid(packet)) {
LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount);
truncatedTargets = true;
break;
}
data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet));
const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet);
if (i < storedHitLimit) {
data.hitTargets.push_back(targetGuid);
}
}
if (truncatedTargets) {
packet.setReadPos(startPos);
return false;
}
data.hitCount = static_cast<uint8_t>(data.hitTargets.size());
// Validate missCount field exists
// missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation.
if (packet.getSize() - packet.getReadPos() < 1) {
return true; // Valid, just no misses
LOG_WARNING("Spell go: missing missCount after hit target list");
packet.setReadPos(startPos);
return false;
}
data.missCount = packet.readUInt8();
// Cap miss count to prevent DoS
if (data.missCount > 128) {
LOG_WARNING("Spell go: missCount capped (requested=", (int)data.missCount, ")");
data.missCount = 128;
const uint8_t rawMissCount = packet.readUInt8();
if (rawMissCount > 128) {
LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount, ")");
}
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
data.missTargets.reserve(data.missCount);
for (uint8_t i = 0; i < data.missCount; ++i) {
data.missTargets.reserve(storedMissLimit);
for (uint16_t i = 0; i < rawMissCount; ++i) {
// Each miss entry: packed GUID(1-8 bytes) + missType(1 byte).
// REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult.
if (packet.getSize() - packet.getReadPos() < 2) {
LOG_WARNING("Spell go: truncated miss targets at index ", (int)i, "/", (int)data.missCount);
data.missCount = i;
if (!hasFullPackedGuid(packet)) {
LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount);
truncatedTargets = true;
break;
}
SpellGoMissEntry m;
m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK
m.missType = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0;
if (packet.getSize() - packet.getReadPos() < 1) {
LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount);
truncatedTargets = true;
break;
}
m.missType = packet.readUInt8();
if (m.missType == 11) {
if (packet.getSize() - packet.getReadPos() < 5) {
LOG_WARNING("Spell go: truncated reflect payload at miss index ", (int)i, "/", (int)data.missCount);
data.missCount = i;
LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount);
truncatedTargets = true;
break;
}
(void)packet.readUInt32();
(void)packet.readUInt8();
}
data.missTargets.push_back(m);
if (i < storedMissLimit) {
data.missTargets.push_back(m);
}
}
if (truncatedTargets) {
packet.setReadPos(startPos);
return false;
}
data.missCount = static_cast<uint8_t>(data.missTargets.size());
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
" misses=", (int)data.missCount);