From 604d53d6d758824dd890d98104e41ddea3ca3509 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Thu, 19 Feb 2026 00:23:04 +0100 Subject: [PATCH] fix(channelserver): validate packet fields before use in handlers Several handlers used packet fields as array indices or SQL column names without bounds checking, allowing crafted packets to panic the server or produce malformed SQL. Panic fixes (high severity): - handlers_mail: bounds check AccIndex against mailList length - handlers_misc: validate ArmourID >= 10000 and MogType <= 4 - handlers_mercenary: check RawDataPayload length before slicing - handlers_house: check RawDataPayload length in SaveDecoMyset - handlers_register: guard empty RawDataPayload in OperateRegister SQL column name fixes (medium severity): - handlers_misc: early return on unknown PointType - handlers_items: reject unknown StampType in weekly stamp handlers - handlers_achievement: cap AchievementID at 32 - handlers_goocoo: skip goocoo.Index > 4 - handlers_house: cap BoxIndex for warehouse operations - handlers_tower: fix MissionIndex=0 bypassing normalization guard --- server/channelserver/handlers_achievement.go | 3 +++ server/channelserver/handlers_goocoo.go | 3 +++ server/channelserver/handlers_house.go | 17 +++++++++++++++++ server/channelserver/handlers_items.go | 8 ++++++++ server/channelserver/handlers_mail.go | 8 ++++++++ server/channelserver/handlers_mercenary.go | 4 ++++ server/channelserver/handlers_misc.go | 15 +++++++++++++-- server/channelserver/handlers_register.go | 4 ++++ server/channelserver/handlers_tower.go | 7 ++----- 9 files changed, 62 insertions(+), 7 deletions(-) diff --git a/server/channelserver/handlers_achievement.go b/server/channelserver/handlers_achievement.go index d6ab68edb..ff909b99e 100644 --- a/server/channelserver/handlers_achievement.go +++ b/server/channelserver/handlers_achievement.go @@ -154,6 +154,9 @@ func handleMsgMhfResetAchievement(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfAddAchievement(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAddAchievement) + if pkt.AchievementID > 32 { + return + } var exists int err := s.server.db.QueryRow("SELECT id FROM achievements WHERE id=$1", s.charID).Scan(&exists) diff --git a/server/channelserver/handlers_goocoo.go b/server/channelserver/handlers_goocoo.go index 151ed88ca..9cef971b2 100644 --- a/server/channelserver/handlers_goocoo.go +++ b/server/channelserver/handlers_goocoo.go @@ -41,6 +41,9 @@ func handleMsgMhfEnumerateGuacot(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateGuacot(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateGuacot) for _, goocoo := range pkt.Goocoos { + if goocoo.Index > 4 { + continue + } if goocoo.Data1[0] == 0 { if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE goocoo SET goocoo%d=NULL WHERE id=$1", goocoo.Index), s.charID); err != nil { s.logger.Error("Failed to clear goocoo slot", zap.Error(err)) diff --git a/server/channelserver/handlers_house.go b/server/channelserver/handlers_house.go index 74e5510c3..5d608a2c8 100644 --- a/server/channelserver/handlers_house.go +++ b/server/channelserver/handlers_house.go @@ -277,6 +277,10 @@ func handleMsgMhfLoadDecoMyset(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSaveDecoMyset(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSaveDecoMyset) + if len(pkt.RawDataPayload) < 3 { + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } var temp []byte err := s.server.db.QueryRow("SELECT decomyset FROM characters WHERE id = $1", s.charID).Scan(&temp) if err != nil { @@ -432,6 +436,9 @@ func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) { case 1: bf.WriteUint8(0) case 2: + if pkt.BoxIndex > 9 { + break + } switch pkt.BoxType { case 0: if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE warehouse SET item%dname=$1 WHERE character_id=$2", pkt.BoxIndex), pkt.Name, s.charID); err != nil { @@ -472,6 +479,9 @@ func warehouseGetItems(s *Session, index uint8) []mhfitem.MHFItemStack { initializeWarehouse(s) var data []byte var items []mhfitem.MHFItemStack + if index > 10 { + return items + } _ = s.server.db.QueryRow(fmt.Sprintf(`SELECT item%d FROM warehouse WHERE character_id=$1`, index), s.charID).Scan(&data) if len(data) > 0 { box := byteframe.NewByteFrameFromBytes(data) @@ -487,6 +497,9 @@ func warehouseGetItems(s *Session, index uint8) []mhfitem.MHFItemStack { func warehouseGetEquipment(s *Session, index uint8) []mhfitem.MHFEquipment { var data []byte var equipment []mhfitem.MHFEquipment + if index > 10 { + return equipment + } _ = s.server.db.QueryRow(fmt.Sprintf(`SELECT equip%d FROM warehouse WHERE character_id=$1`, index), s.charID).Scan(&data) if len(data) > 0 { box := byteframe.NewByteFrameFromBytes(data) @@ -519,6 +532,10 @@ func handleMsgMhfEnumerateWarehouse(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateWarehouse(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateWarehouse) + if pkt.BoxIndex > 10 { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } saveStart := time.Now() var err error diff --git a/server/channelserver/handlers_items.go b/server/channelserver/handlers_items.go index 0bcd5bcd2..475111b42 100644 --- a/server/channelserver/handlers_items.go +++ b/server/channelserver/handlers_items.go @@ -225,6 +225,10 @@ func handleMsgMhfGetCogInfo(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfCheckWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCheckWeeklyStamp) + if pkt.StampType != "hl" && pkt.StampType != "ex" { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 14)) + return + } var total, redeemed, updated uint16 var lastCheck time.Time err := s.server.db.QueryRow(fmt.Sprintf("SELECT %s_checked FROM stamps WHERE character_id=$1", pkt.StampType), s.charID).Scan(&lastCheck) @@ -259,6 +263,10 @@ func handleMsgMhfCheckWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfExchangeWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfExchangeWeeklyStamp) + if pkt.StampType != "hl" && pkt.StampType != "ex" { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 12)) + return + } var total, redeemed uint16 var tktStack mhfitem.MHFItemStack if pkt.ExchangeType == 10 { // Yearly Sub Ex diff --git a/server/channelserver/handlers_mail.go b/server/channelserver/handlers_mail.go index d226ba6ea..77fadc33a 100644 --- a/server/channelserver/handlers_mail.go +++ b/server/channelserver/handlers_mail.go @@ -208,6 +208,10 @@ func getCharacterName(s *Session, charID uint32) string { func handleMsgMhfReadMail(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfReadMail) + if int(pkt.AccIndex) >= len(s.mailList) { + doAckBufSucceed(s, pkt.AckHandle, []byte{0}) + return + } mailId := s.mailList[pkt.AccIndex] if mailId == 0 { doAckBufSucceed(s, pkt.AckHandle, []byte{0}) @@ -301,6 +305,10 @@ func handleMsgMhfListMail(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfOprtMail(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfOprtMail) + if int(pkt.AccIndex) >= len(s.mailList) { + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } mail, err := GetMailByID(s, s.mailList[pkt.AccIndex]) if err != nil { doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) diff --git a/server/channelserver/handlers_mercenary.go b/server/channelserver/handlers_mercenary.go index cc2d6102c..9ecc355de 100644 --- a/server/channelserver/handlers_mercenary.go +++ b/server/channelserver/handlers_mercenary.go @@ -307,6 +307,10 @@ func handleMsgMhfLoadOtomoAirou(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSaveOtomoAirou(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSaveOtomoAirou) + if len(pkt.RawDataPayload) < 2 { + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } dumpSaveData(s, pkt.RawDataPayload, "otomoairou") decomp, err := nullcomp.Decompress(pkt.RawDataPayload[1:]) if err != nil { diff --git a/server/channelserver/handlers_misc.go b/server/channelserver/handlers_misc.go index a74d1878a..970a2bab9 100644 --- a/server/channelserver/handlers_misc.go +++ b/server/channelserver/handlers_misc.go @@ -43,6 +43,9 @@ func handleMsgMhfUpdateEtcPoint(s *Session, p mhfpacket.MHFPacket) { column = "daily_quests" case 2: column = "promo_points" + default: + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return } var value int16 @@ -187,9 +190,17 @@ func handleMsgMhfUpdateEquipSkinHist(s *Session, p mhfpacket.MHFPacket) { return } + if pkt.ArmourID < 10000 || pkt.MogType > 4 { + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } bit := int(pkt.ArmourID) - 10000 - startByte := (size / 5) * int(pkt.MogType) - // psql set_bit could also work but I couldn't get it working + sectionSize := size / 5 + if bit/8 >= sectionSize { + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } + startByte := sectionSize * int(pkt.MogType) byteInd := bit / 8 bitInByte := bit % 8 data[startByte+byteInd] |= bits.Reverse8(1 << uint(bitInByte)) diff --git a/server/channelserver/handlers_register.go b/server/channelserver/handlers_register.go index 04df759c9..e7e870f1b 100644 --- a/server/channelserver/handlers_register.go +++ b/server/channelserver/handlers_register.go @@ -58,6 +58,10 @@ type RaviUpdate struct { func handleMsgSysOperateRegister(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysOperateRegister) + if len(pkt.RawDataPayload) == 0 { + return + } + var raviUpdates []RaviUpdate var raviUpdate RaviUpdate // Strip null terminator diff --git a/server/channelserver/handlers_tower.go b/server/channelserver/handlers_tower.go index cb9a809b4..3114d8dfd 100644 --- a/server/channelserver/handlers_tower.go +++ b/server/channelserver/handlers_tower.go @@ -329,11 +329,8 @@ func handleMsgMhfGetTenrouirai(s *Session, p mhfpacket.MHFPacket) { data = append(data, bf) } case 5: - if pkt.MissionIndex > 3 { - pkt.MissionIndex %= 3 - if pkt.MissionIndex == 0 { - pkt.MissionIndex = 3 - } + if pkt.MissionIndex < 1 || pkt.MissionIndex > 3 { + pkt.MissionIndex = (pkt.MissionIndex % 3) + 1 } rows, err := s.server.db.Query(fmt.Sprintf(`SELECT name, tower_mission_%d FROM guild_characters gc INNER JOIN characters c ON gc.character_id = c.id WHERE guild_id=$1 AND tower_mission_%d IS NOT NULL ORDER BY tower_mission_%d DESC`, pkt.MissionIndex, pkt.MissionIndex, pkt.MissionIndex), pkt.GuildID) if err != nil {