mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
feat(savedata): add tier 1 data integrity protections
Prevent savedata corruption and denial-of-service by adding four layers of protection to the save pipeline: - Bounded decompression (nullcomp.DecompressWithLimit): caps output size to prevent OOM from crafted payloads that expand to exhaust memory - Bounds-checked delta patching (deltacomp.ApplyDataDiffWithLimit): validates offsets before writing, returns errors for negative offsets, truncated patches, and oversized output; ApplyDataDiff now returns original data on error instead of partial corruption - Size limits on save handlers: rejects compressed payloads >512KB and decompressed data >1MB before processing; applied to main savedata, platedata, and platebox diff paths - Rotating savedata backups: 3 slots per character with 30-minute interval, snapshots the previous state before overwriting, backed by new savedata_backups table (migration 0007)
This commit is contained in:
@@ -2,6 +2,7 @@ package nullcomp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
@@ -49,6 +50,61 @@ func Decompress(compData []byte) ([]byte, error) {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// DecompressWithLimit decompresses null-compressed data, returning an error if
|
||||
// the decompressed output would exceed maxOutput bytes. This prevents
|
||||
// denial-of-service via crafted payloads that expand to exhaust memory.
|
||||
func DecompressWithLimit(compData []byte, maxOutput int) ([]byte, error) {
|
||||
r := bytes.NewReader(compData)
|
||||
|
||||
header := make([]byte, 16)
|
||||
n, err := r.Read(header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if n != len(header) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Just return the data if it doesn't contain the cmp header.
|
||||
if !bytes.Equal(header, []byte("cmp\x2020110113\x20\x20\x20\x00")) {
|
||||
if len(compData) > maxOutput {
|
||||
return nil, fmt.Errorf("uncompressed data size %d exceeds limit %d", len(compData), maxOutput)
|
||||
}
|
||||
return compData, nil
|
||||
}
|
||||
|
||||
var output []byte
|
||||
for {
|
||||
b, err := r.ReadByte()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b == 0 {
|
||||
// If it's a null byte, then the next byte is how many nulls to add.
|
||||
nullCount, err := r.ReadByte()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(output)+int(nullCount) > maxOutput {
|
||||
return nil, fmt.Errorf("decompressed size exceeds limit %d", maxOutput)
|
||||
}
|
||||
output = append(output, make([]byte, int(nullCount))...)
|
||||
} else {
|
||||
if len(output)+1 > maxOutput {
|
||||
return nil, fmt.Errorf("decompressed size exceeds limit %d", maxOutput)
|
||||
}
|
||||
output = append(output, b)
|
||||
}
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Compress null compresses give given data.
|
||||
func Compress(rawData []byte) ([]byte, error) {
|
||||
r := bytes.NewReader(rawData)
|
||||
|
||||
Reference in New Issue
Block a user