This commit introduces a fuzz test for the `Decode` function in the `trix` package. This test immediately uncovered a critical out-of-memory (OOM) vulnerability. - Adds a new fuzz test, `FuzzDecode`, to `pkg/trix/fuzz_test.go` to continuously test the `Decode` function with a wide range of malformed inputs. - Fixes a denial-of-service vulnerability where a malicious input could specify an extremely large header length, causing the application to crash due to an out-of-memory error. - Introduces a `MaxHeaderSize` constant (16MB) and a check in the `Decode` function to ensure that the header length does not exceed this limit. - Adds a new error, `ErrHeaderTooLarge`, to provide clear feedback when the header size limit is exceeded.
198 lines
4.7 KiB
Go
198 lines
4.7 KiB
Go
package trix
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
|
)
|
|
|
|
const (
|
|
Version = 2
|
|
MaxHeaderSize = 16 * 1024 * 1024 // 16 MB
|
|
)
|
|
|
|
var (
|
|
ErrInvalidMagicNumber = errors.New("trix: invalid magic number")
|
|
ErrInvalidVersion = errors.New("trix: invalid version")
|
|
ErrMagicNumberLength = errors.New("trix: magic number must be 4 bytes long")
|
|
ErrNilSigil = errors.New("trix: sigil cannot be nil")
|
|
ErrChecksumMismatch = errors.New("trix: checksum mismatch")
|
|
ErrHeaderTooLarge = errors.New("trix: header size exceeds maximum allowed")
|
|
)
|
|
|
|
// Trix represents the structure of a .trix file.
|
|
type Trix struct {
|
|
Header map[string]interface{}
|
|
Payload []byte
|
|
InSigils []string `json:"-"` // Ignore Sigils during JSON marshaling
|
|
OutSigils []string `json:"-"` // Ignore Sigils during JSON marshaling
|
|
ChecksumAlgo crypt.HashType `json:"-"`
|
|
}
|
|
|
|
// Encode serializes a Trix struct into the .trix binary format.
|
|
func Encode(trix *Trix, magicNumber string) ([]byte, error) {
|
|
if len(magicNumber) != 4 {
|
|
return nil, ErrMagicNumberLength
|
|
}
|
|
|
|
// Calculate and add checksum if an algorithm is specified
|
|
if trix.ChecksumAlgo != "" {
|
|
checksum := crypt.NewService().Hash(trix.ChecksumAlgo, string(trix.Payload))
|
|
trix.Header["checksum"] = checksum
|
|
trix.Header["checksum_algo"] = string(trix.ChecksumAlgo)
|
|
}
|
|
|
|
headerBytes, err := json.Marshal(trix.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headerLength := uint32(len(headerBytes))
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
// Write Magic Number
|
|
if _, err := buf.WriteString(magicNumber); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write Version
|
|
if err := buf.WriteByte(byte(Version)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write Header Length
|
|
if err := binary.Write(buf, binary.BigEndian, headerLength); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write JSON Header
|
|
if _, err := buf.Write(headerBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write Payload
|
|
if _, err := buf.Write(trix.Payload); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// Decode deserializes the .trix binary format into a Trix struct.
|
|
// Note: Sigils are not stored in the format and must be re-attached by the caller.
|
|
func Decode(data []byte, magicNumber string) (*Trix, error) {
|
|
if len(magicNumber) != 4 {
|
|
return nil, ErrMagicNumberLength
|
|
}
|
|
|
|
buf := bytes.NewReader(data)
|
|
|
|
// Read and Verify Magic Number
|
|
magic := make([]byte, 4)
|
|
if _, err := io.ReadFull(buf, magic); err != nil {
|
|
return nil, err
|
|
}
|
|
if string(magic) != magicNumber {
|
|
return nil, fmt.Errorf("%w: expected %s, got %s", ErrInvalidMagicNumber, magicNumber, string(magic))
|
|
}
|
|
|
|
// Read and Verify Version
|
|
version, err := buf.ReadByte()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if version != Version {
|
|
return nil, ErrInvalidVersion
|
|
}
|
|
|
|
// Read Header Length
|
|
var headerLength uint32
|
|
if err := binary.Read(buf, binary.BigEndian, &headerLength); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sanity check the header length to prevent massive allocations.
|
|
if headerLength > MaxHeaderSize {
|
|
return nil, ErrHeaderTooLarge
|
|
}
|
|
|
|
// Read JSON Header
|
|
headerBytes := make([]byte, headerLength)
|
|
if _, err := io.ReadFull(buf, headerBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
var header map[string]interface{}
|
|
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read Payload
|
|
payload, err := io.ReadAll(buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify checksum if it exists in the header
|
|
if checksum, ok := header["checksum"].(string); ok {
|
|
algo, ok := header["checksum_algo"].(string)
|
|
if !ok {
|
|
return nil, errors.New("trix: checksum algorithm not found in header")
|
|
}
|
|
expectedChecksum := crypt.NewService().Hash(crypt.HashType(algo), string(payload))
|
|
if checksum != expectedChecksum {
|
|
return nil, ErrChecksumMismatch
|
|
}
|
|
}
|
|
|
|
return &Trix{
|
|
Header: header,
|
|
Payload: payload,
|
|
}, nil
|
|
}
|
|
|
|
// Pack applies the In method of all attached sigils to the payload.
|
|
func (t *Trix) Pack() error {
|
|
for _, sigilName := range t.InSigils {
|
|
sigil, err := enchantrix.NewSigil(sigilName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sigil == nil {
|
|
return ErrNilSigil
|
|
}
|
|
t.Payload, err = sigil.In(t.Payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unpack applies the Out method of all sigils in reverse order.
|
|
func (t *Trix) Unpack() error {
|
|
sigilNames := t.OutSigils
|
|
if len(sigilNames) == 0 {
|
|
sigilNames = t.InSigils
|
|
}
|
|
for i := len(sigilNames) - 1; i >= 0; i-- {
|
|
sigilName := sigilNames[i]
|
|
sigil, err := enchantrix.NewSigil(sigilName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sigil == nil {
|
|
return ErrNilSigil
|
|
}
|
|
t.Payload, err = sigil.Out(t.Payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|