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
This commit is contained in:
Houmgaor
2026-02-19 00:23:04 +01:00
parent 99e544e0cf
commit 604d53d6d7
9 changed files with 62 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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