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/file_resume_data.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/file_resume_data.go')
| -rw-r--r-- | hotline/file_resume_data.go | 22 |
1 files changed, 19 insertions, 3 deletions
diff --git a/hotline/file_resume_data.go b/hotline/file_resume_data.go index 1926bc6..7cbfb17 100644 --- a/hotline/file_resume_data.go +++ b/hotline/file_resume_data.go @@ -76,15 +76,31 @@ func (frd *FileResumeData) BinaryMarshal() ([]byte, error) { return buf.Bytes(), nil } +// resumeDataHeaderLen is the fixed-size header (Format, Version, RSVD, ForkCount) +// that precedes the variable-length fork info list. +const resumeDataHeaderLen = 42 + +// forkInfoLen is the size of a single ForkInfoList entry. +const forkInfoLen = 16 + func (frd *FileResumeData) UnmarshalBinary(b []byte) error { + if len(b) < resumeDataHeaderLen { + return fmt.Errorf("file resume data too short: %d bytes, need at least %d", len(b), resumeDataHeaderLen) + } + frd.Format = [4]byte{b[0], b[1], b[2], b[3]} frd.Version = [2]byte{b[4], b[5]} frd.ForkCount = [2]byte{b[40], b[41]} - for i := 0; i < int(frd.ForkCount[1]); i++ { + forkCount := int(frd.ForkCount[1]) + if need := resumeDataHeaderLen + forkCount*forkInfoLen; len(b) < need { + return fmt.Errorf("file resume data truncated: %d bytes, need %d for %d forks", len(b), need, forkCount) + } + + for i := 0; i < forkCount; i++ { var fil ForkInfoList - start := 42 + i*16 - end := start + 16 + start := resumeDataHeaderLen + i*forkInfoLen + end := start + forkInfoLen r := bytes.NewReader(b[start:end]) if err := binary.Read(r, binary.BigEndian, &fil); err != nil { |