Files
larksuite-cli/shortcuts/im/mp4_box_test.go
lhfer e18ea9a2e8 fix(im): correct 64-bit MP4 box size handling to prevent panic on crafted media (#1165)
The size==1 (64-bit "largesize") branch of all three MP4 box walkers
(findMP4Box, readMp4DurationBytes, readMp4Duration) set boxEnd to the raw
largesize instead of offset+largesize — even though the 32-bit branch right
below correctly uses offset+size. Two consequences:

- Correctness: for any MP4 that carries a 64-bit box size at a non-zero
  offset, the box walk is computed from the wrong end, so the moov/mvhd
  lookup is truncated and the media duration is silently lost.

- Robustness/security (CWE-190): the unguarded uint64->int(64) conversion of
  a largesize with the high bit set yields a negative boxEnd. The in-memory
  walkers then assign it to offset and feed it back as a slice index
  (data[offset:]), panicking with "slice bounds out of range" and crashing
  the CLI on a crafted or corrupt MP4. This is reachable via URL-sourced IM
  media, whose bytes the caller does not control.

Fix: compute boxEnd as offset+largesize (matching the 32-bit branch) and
reject largesize values smaller than the 16-byte header or larger than the
remaining input. Malformed media now honours the parsers' best-effort
contract by returning 0/-1 instead of panicking, and the bounds guarantee
the conversion can no longer overflow.

Add regression tests covering both the overflow (must not panic) and a
64-bit box at a non-zero offset (must walk correctly).
2026-05-29 16:04:21 +08:00

86 lines
3.6 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package im
import (
"encoding/binary"
"testing"
)
// build64BitBox builds an ISO-BMFF box using the 64-bit "largesize" form: the
// 32-bit size field is set to 1 and an 8-byte largesize follows the 4-byte box
// type. largesize is the total box length including the 16-byte header.
func build64BitBox(boxType string, largesize uint64, payload []byte) []byte {
box := make([]byte, 16+len(payload))
binary.BigEndian.PutUint32(box[0:4], 1) // size == 1 → 64-bit largesize follows
copy(box[4:8], boxType)
binary.BigEndian.PutUint64(box[8:16], largesize)
copy(box[16:], payload)
return box
}
// build32BitBox builds an ISO-BMFF box using the ordinary 32-bit size form.
func build32BitBox(boxType string, payload []byte) []byte {
box := make([]byte, 8+len(payload))
binary.BigEndian.PutUint32(box[0:4], uint32(len(box)))
copy(box[4:8], boxType)
copy(box[8:], payload)
return box
}
// TestMP4BoxLargeSizeOverflowNoPanic guards the 64-bit box-size branch against
// CWE-190 integer overflow. A largesize whose high bit is set converts to a
// negative offset; without a bounds guard that offset indexes the input slice
// out of range and panics, crashing the CLI on a crafted/corrupt MP4 (the
// in-memory walkers run on URL-sourced media that the caller does not control).
// The walkers' contract is best-effort: malformed input must return 0, not panic.
func TestMP4BoxLargeSizeOverflowNoPanic(t *testing.T) {
// A single top-level box in the 64-bit form with largesize = 2^64-1.
data := build64BitBox("ftyp", 0xFFFFFFFFFFFFFFFF, nil)
if got := readMp4DurationBytes(data); got != 0 {
t.Errorf("readMp4DurationBytes(overflow largesize) = %d, want 0", got)
}
if got := parseMp4Duration(data); got != 0 {
t.Errorf("parseMp4Duration(overflow largesize) = %d, want 0", got)
}
if start, end := findMP4Box(data, 0, len(data), "ftyp"); start != -1 || end != -1 {
t.Errorf("findMP4Box(overflow largesize) = (%d, %d), want (-1, -1)", start, end)
}
}
// TestMP4Box64BitSizeAtNonZeroOffset locks in correct handling of a 64-bit box
// that does not start at offset 0. boxEnd must be offset+largesize (as the
// 32-bit branch already does with offset+size); dropping the offset truncates
// the box and the duration is silently lost.
func TestMP4Box64BitSizeAtNonZeroOffset(t *testing.T) {
mvhd := buildMvhdBox(0, 1000, 5000) // timescale=1000, duration=5000 → 5000ms
// moov carried as a 64-bit box: largesize = 16-byte header + mvhd payload.
moov := build64BitBox("moov", uint64(16+len(mvhd)), mvhd)
// Precede moov with a 32-bit ftyp box so it sits at a non-zero offset —
// that is where the missing "offset +" surfaces.
data := append(build32BitBox("ftyp", []byte("isom")), moov...)
if got := readMp4DurationBytes(data); got != 5000 {
t.Errorf("readMp4DurationBytes(64-bit moov at offset>0) = %d, want 5000", got)
}
}
// TestFindMP4Box64BitSizeAtNonZeroOffset is the findMP4Box-level analogue: a
// 64-bit box preceding the target must advance the cursor by offset+largesize
// so the following box is located at the right position.
func TestFindMP4Box64BitSizeAtNonZeroOffset(t *testing.T) {
free := build64BitBox("free", 24, make([]byte, 8)) // 16-byte header + 8 bytes
target := build32BitBox("mvhd", []byte("payload!"))
data := append(free, target...)
start, end := findMP4Box(data, 0, len(data), "mvhd")
if start < 0 {
t.Fatalf("findMP4Box did not find mvhd after a 64-bit box (start=%d)", start)
}
if got := string(data[start:end]); got != "payload!" {
t.Errorf("findMP4Box returned %q, want %q", got, "payload!")
}
}