diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-05-31 14:48:31 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-05-31 14:48:31 -0700 |
| commit | 123d1a305bc68474034f5989362148508bdbf7f5 (patch) | |
| tree | 341a0030c5a1a9abfaa73fa31bd59c7e5c216bf7 /hotline/flattened_file_object.go | |
| parent | 588dce918aeda0efc4db80b30525d28943c029cd (diff) | |
Validate client input to prevent panics from malformed transactions
A malicious or buggy client could send transaction fields with the wrong
length and trigger runtime panics (slice/index out of range, slice-to-array
conversion, nil deref) in the parsing and handler code. These were caught by
the connection-level recover, so they dropped the client connection and dumped
a stack trace to stdout rather than crashing the process, but they are still
incorrect behavior, a log-flood vector, and a latent crash if the recover
scope ever changes.
Fix at the source and harden the safety net:
- Add ClientIDFromBytes / ChatIDFromBytes helpers that return ok=false on a
length mismatch, and use them in the transaction handlers instead of direct
[2]byte(...) / [4]byte(...) conversions on field data. Nil-check ClientMgr.Get
results, and length-guard FieldOptions and the HandleUpdateUser sub-field
header. Malformed input now yields an error reply (or a clean no-op for
reply-less handlers) instead of panicking.
- Bounds-check FileResumeData.UnmarshalBinary (header length and fork count)
and guard the ForkInfoList[0] accesses against an empty list.
- Bounds-check FlatFileInformationFork parsing (reachable on upload): validate
the fixed header, name, and comment lengths, and fix a latent 72+nameSize
uint16 overflow. Route Write through UnmarshalBinary.
- panic.go: stop printing stack traces to stdout (keep structured logging) so a
client cannot flood stdout by repeatedly triggering a panic.
- handleTransaction: recover per-transaction so one malformed request no longer
tears down the whole client connection.
Adds tests for the new helpers, the hardened resume-data and flat-file-object
decoders, and handler-level malformed-ID handling.
Diffstat (limited to 'hotline/flattened_file_object.go')
| -rw-r--r-- | hotline/flattened_file_object.go | 57 |
1 files changed, 25 insertions, 32 deletions
diff --git a/hotline/flattened_file_object.go b/hotline/flattened_file_object.go index 130e917..9a4fd85 100644 --- a/hotline/flattened_file_object.go +++ b/hotline/flattened_file_object.go @@ -3,10 +3,15 @@ package hotline import ( "bytes" "encoding/binary" + "fmt" "io" "slices" ) +// flatFileInfoForkMinLen is the fixed-size portion of a FlatFileInformationFork +// that precedes the variable-length name (and optional comment). +const flatFileInfoForkMinLen = 72 + type flattenedFileObject struct { FlatFileHeader FlatFileHeader FlatFileInformationForkHeader FlatFileForkHeader @@ -156,40 +161,22 @@ func (ffif *FlatFileInformationFork) Read(p []byte) (int, error) { // Write implements the io.Writer interface for FlatFileInformationFork func (ffif *FlatFileInformationFork) Write(p []byte) (int, error) { - nameSize := p[70:72] - bs := binary.BigEndian.Uint16(nameSize) - total := 72 + bs - - ffif.Platform = [4]byte(p[0:4]) - ffif.TypeSignature = [4]byte(p[4:8]) - ffif.CreatorSignature = [4]byte(p[8:12]) - ffif.Flags = [4]byte(p[12:16]) - ffif.PlatformFlags = [4]byte(p[16:20]) - ffif.RSVD = [32]byte(p[20:52]) - ffif.CreateDate = [8]byte(p[52:60]) - ffif.ModifyDate = [8]byte(p[60:68]) - ffif.NameScript = [2]byte(p[68:70]) - ffif.NameSize = [2]byte(p[70:72]) - ffif.Name = p[72:total] - - if len(p) > int(total) { - ffif.CommentSize = [2]byte(p[total : total+2]) - commentLen := binary.BigEndian.Uint16(ffif.CommentSize[:]) - commentStartPos := int(total) + 2 - commentEndPos := int(total) + 2 + int(commentLen) - - ffif.Comment = p[commentStartPos:commentEndPos] - - //total = uint16(commentEndPos) + if err := ffif.UnmarshalBinary(p); err != nil { + return 0, err } - return len(p), nil } func (ffif *FlatFileInformationFork) UnmarshalBinary(b []byte) error { - nameSize := b[70:72] - bs := binary.BigEndian.Uint16(nameSize) - nameEnd := 72 + bs + if len(b) < flatFileInfoForkMinLen { + return fmt.Errorf("flat file information fork too short: %d bytes, need at least %d", len(b), flatFileInfoForkMinLen) + } + + bs := binary.BigEndian.Uint16(b[70:72]) + nameEnd := flatFileInfoForkMinLen + int(bs) + if len(b) < nameEnd { + return fmt.Errorf("flat file information fork name overruns buffer: need %d bytes, have %d", nameEnd, len(b)) + } ffif.Platform = [4]byte(b[0:4]) ffif.TypeSignature = [4]byte(b[4:8]) @@ -203,12 +190,18 @@ func (ffif *FlatFileInformationFork) UnmarshalBinary(b []byte) error { ffif.NameSize = [2]byte(b[70:72]) ffif.Name = b[72:nameEnd] - if len(b) > int(nameEnd) { + if len(b) > nameEnd { + if len(b) < nameEnd+2 { + return fmt.Errorf("flat file information fork comment size overruns buffer") + } ffif.CommentSize = [2]byte(b[nameEnd : nameEnd+2]) commentLen := binary.BigEndian.Uint16(ffif.CommentSize[:]) - commentStartPos := int(nameEnd) + 2 - commentEndPos := int(nameEnd) + 2 + int(commentLen) + commentStartPos := nameEnd + 2 + commentEndPos := nameEnd + 2 + int(commentLen) + if len(b) < commentEndPos { + return fmt.Errorf("flat file information fork comment overruns buffer: need %d bytes, have %d", commentEndPos, len(b)) + } ffif.Comment = b[commentStartPos:commentEndPos] } |