From 2770b46ab7cd082b0b46fd32595dc8b8cc07473b Mon Sep 17 00:00:00 2001 From: Palapeli <26661008+mkwcat@users.noreply.github.com> Date: Sat, 4 Apr 2026 01:46:41 -0400 Subject: [PATCH] Sake: Sanitize Mii info returned from mkw FriendInfo --- sake/mario_kart_wii.go | 28 ++++++++++++++++++++++++++++ sake/storage.go | 41 ++++++++++++++++++++++++++++++++--------- sake/tables.go | 38 +++++++++++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 12 deletions(-) diff --git a/sake/mario_kart_wii.go b/sake/mario_kart_wii.go index f3e7731..317acf5 100644 --- a/sake/mario_kart_wii.go +++ b/sake/mario_kart_wii.go @@ -453,6 +453,34 @@ func isPlayerInfoValid(playerInfoString string) (string, bool) { return fixedPlayerInfoString, true } +func filterMarioKartWiiFriendInfo(value string, isOwner bool) (string, string) { + // Clear personal info from the Mii data before sending to the client. This + // cannot be done before inserting into the database because the client + // expects the server to return the exact copy of what it has previously updated. + if isOwner { + return value, ResultSuccess + } + + binaryData, err := base64.StdEncoding.DecodeString(value) + if err != nil { + // Shouldn't happen after validation + panic(err) + } + // Only change if the Mii checksum is valid + if len(binaryData) < 0x4C { + return value, ResultSuccess + } + + mii := common.RawMiiFromBytes(binaryData) + if mii.CalculateMiiCRC() != 0 { + return value, ResultSuccess + } + + mii = mii.ClearMiiInfo() + binaryData = append(mii.Data[:], binaryData[len(mii.Data):]...) + return base64.StdEncoding.EncodeToString(binaryData), ResultSuccess +} + func getMultipartBoundary(contentType string) string { startIndex := strings.Index(contentType, "boundary=") if startIndex == -1 { diff --git a/sake/storage.go b/sake/storage.go index 3ce373e..fbf945d 100644 --- a/sake/storage.go +++ b/sake/storage.go @@ -306,6 +306,14 @@ func getMyRecords(moduleName string, profileId uint32, gameInfo common.GameInfo, }} } + table := GetTable(gameInfo.Name, request.TableID) + if table != nil && table.Reserved { + logging.Error(moduleName, "Attempt to get my records from reserved table", aurora.Cyan(request.TableID)) + return StorageResponseBody{GetMyRecordsResponse: &StorageGetMyRecordsResponse{ + GetMyRecordsResult: ResultTableNotFound, + }} + } + records, err := database.GetSakeRecords(pool, ctx, gameInfo.GameID, []int32{int32(profileId)}, request.TableID, nil, request.Fields.Fields, request.Filter) if err != nil { logging.Error(moduleName, "Failed to get sake records from the database:", err) @@ -319,9 +327,10 @@ func getMyRecords(moduleName string, profileId uint32, gameInfo common.GameInfo, }} } + responseValues, result := fillResponseValues(moduleName, profileId, table, records, request) response := StorageGetMyRecordsResponse{ - GetMyRecordsResult: ResultSuccess, - Values: fillResponseValues(records, request), + GetMyRecordsResult: result, + Values: responseValues, } logging.Info(moduleName, "Returning", aurora.Cyan(len(records)), "records from table", aurora.Cyan(request.TableID), "for profile", aurora.Cyan(profileId)) @@ -462,9 +471,10 @@ func searchForRecords(moduleName string, profileId uint32, gameInfo common.GameI records = records[:request.Max] } + responseValues, result := fillResponseValues(moduleName, profileId, table, records, request) response := StorageSearchForRecordsResponse{ - SearchForRecordsResult: "Success", - Values: fillResponseValues(records, request), + SearchForRecordsResult: result, + Values: responseValues, } logging.Info(moduleName, "Found", aurora.Cyan(len(records)), "records in table", aurora.Cyan(request.TableID), "for profile", aurora.Cyan(profileId), "with filter", aurora.Cyan(request.Filter)) @@ -502,12 +512,18 @@ func getInputFields(moduleName string, request StorageRequestData, table *SakeTa logging.Error(moduleName, "Invalid value for field", aurora.Cyan(field.Name).String()+":", aurora.Cyan(value)) return nil, result } + var result string + sakeField.Value, result = table.FilterFieldFromClient(field.Name, value) + if result != ResultSuccess { + logging.Error(moduleName, "Failed to filter from client value for field", aurora.Cyan(field.Name), ":", aurora.Cyan(result)) + return nil, result + } fields[field.Name] = sakeField } return fields, ResultSuccess } -func fillResponseValues(records []database.SakeRecord, request StorageRequestData) StorageResponseValues { +func fillResponseValues(moduleName string, profileId uint32, table *SakeTable, records []database.SakeRecord, request StorageRequestData) (StorageResponseValues, string) { var response StorageResponseValues for _, record := range records { valueArray := StorageArrayOfRecordValue{} @@ -536,14 +552,21 @@ func fillResponseValues(records []database.SakeRecord, request StorageRequestDat } if fieldValue == nil { valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: nil}) - } else { - value := fillValue(fieldValue.Type, fieldValue.Value) - valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: &value}) + continue } + + var result string + fieldValue.Value, result = table.FilterFieldFromDatabase(field, fieldValue.Value, record.OwnerId == int32(profileId)) + if result != ResultSuccess { + logging.Error(moduleName, "Failed to filter to client value for field", aurora.Cyan(field), ":", aurora.Cyan(result)) + return StorageResponseValues{}, result + } + value := fillValue(fieldValue.Type, fieldValue.Value) + valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: &value}) } response.ArrayOfRecordValue = append(response.ArrayOfRecordValue, valueArray) } - return response + return response, ResultSuccess } func fillValue(valueType database.SakeFieldType, value string) StorageValue { diff --git a/sake/tables.go b/sake/tables.go index 907ae35..3b77dea 100644 --- a/sake/tables.go +++ b/sake/tables.go @@ -51,9 +51,9 @@ type SakeFieldDefinition struct { // Optional function for custom validation. IsValidFunc func(value string) bool // Optional function for custom filtering. This function receives the value from the client AFTER validation, before inserting into the database. - FilterFromClientFunc func(value string) (string, error) + FilterFromClientFunc func(value string, isOwner bool) (string, string) // Optional function for custom filtering. This function receives the value from the database before sending to the client. - FilterFromDatabaseFunc func(value string) (string, error) + FilterFromDatabaseFunc func(value string, isOwner bool) (string, string) } type SakeTable struct { @@ -113,7 +113,8 @@ var TableDefinitions = map[string]SakeTable{ Hardened: false, // To allow modding extra fields Fields: map[string]SakeFieldDefinition{ "info": { - Type: database.SakeFieldTypeBinaryData, + Type: database.SakeFieldTypeBinaryData, + FilterFromDatabaseFunc: filterMarioKartWiiFriendInfo, }, }, }, @@ -387,3 +388,34 @@ func (t *SakeTable) CheckValidField(fieldName string, field database.SakeField) } return ResultSuccess } + +func (t *SakeTable) FilterFieldFromClient(fieldName string, value string) (string, string) { + if t == nil || t.Fields == nil { + return value, ResultSuccess + } + fieldDef, exists := t.Fields[fieldName] + if !exists { + return value, ResultSuccess + } + if fieldDef.FilterFromClientFunc == nil { + return value, ResultSuccess + } + return fieldDef.FilterFromClientFunc(value, true) +} + +func (t *SakeTable) FilterFieldFromDatabase(fieldName string, value string, isOwner bool) (string, string) { + if t == nil || t.Fields == nil { + return value, ResultSuccess + } + fieldDef, exists := t.Fields[fieldName] + if !exists { + return value, ResultSuccess + } + if fieldDef.FilterFromDatabaseFunc == nil { + return value, ResultSuccess + } + if t.OwnerType != OwnerTypeProfile { + isOwner = false + } + return fieldDef.FilterFromDatabaseFunc(value, isOwner) +}