aboutsummaryrefslogtreecommitdiff
path: root/hotline/flattened_file_object.go
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-05-31 14:48:31 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-05-31 14:48:31 -0700
commit123d1a305bc68474034f5989362148508bdbf7f5 (patch)
tree341a0030c5a1a9abfaa73fa31bd59c7e5c216bf7 /hotline/flattened_file_object.go
parent588dce918aeda0efc4db80b30525d28943c029cd (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.go57
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]
}