mirror of
https://github.com/anchore/syft.git
synced 2026-08-19 08:38:25 +02:00
fix(arch): bound the decompressed mtree listing
The ALPM cataloger handed a gzip stream straight to `mtree.ParseSpec`, which materializes every entry before it returns any of them. Nothing capped the decompressed size, so a crafted mtree in a scanned image expanded until the process died. 64KB of input is enough to produce 64MB, and the ratio scales. Reads through a limit now and rejects a listing that exceeds it. The read goes one byte past the cap so that tripping it is distinguishable from a listing that simply ends there, since silently truncating would produce a package missing most of its files. The existing error path already surfaces this as an unknown. The cap is well above any real package: an mtree names every file with its digests, and even the largest packages land far under it. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
68da404bd7
commit
b8381670d6
@ -2,6 +2,7 @@ package arch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
@ -277,15 +278,31 @@ func parsePkgFiles(pkgFields map[string]any) (*parsedData, error) {
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
// maxMtreeSize bounds the decompressed mtree listing. An mtree names every file in a package along
|
||||
// with its digests, so even a very large package lands well under this. The cap is here because gzip
|
||||
// reaches roughly 1032:1, so without it a small crafted member expands until the process dies, and
|
||||
// mtree.ParseSpec materializes every entry before returning any of them.
|
||||
const maxMtreeSize = 64 * 1024 * 1024
|
||||
|
||||
func parseMtree(r io.Reader) ([]pkg.AlpmFileRecord, error) {
|
||||
var err error
|
||||
var entries []pkg.AlpmFileRecord
|
||||
|
||||
r, err = gzip.NewReader(r)
|
||||
gzReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
specDh, err := mtree.ParseSpec(r)
|
||||
|
||||
// read one byte past the cap so that hitting it is distinguishable from a listing that simply ends
|
||||
// there. Truncating instead would hand back a package silently missing most of its files.
|
||||
data, err := io.ReadAll(io.LimitReader(gzReader, maxMtreeSize+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) > maxMtreeSize {
|
||||
return nil, fmt.Errorf("mtree file is larger than the max allowed size (%d bytes)", maxMtreeSize)
|
||||
}
|
||||
|
||||
specDh, err := mtree.ParseSpec(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ package arch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@ -217,3 +220,48 @@ func TestMtreeParse(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// gzipOfSize returns a gzip member that decompresses to exactly n bytes. The payload is
|
||||
// highly compressible, which is the whole point: the caller supplies kilobytes and the
|
||||
// decompressed stream is whatever size it asks for.
|
||||
func gzipOfSize(t *testing.T, n int64) io.Reader {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
chunk := make([]byte, 32*1024)
|
||||
for remaining := n; remaining > 0; {
|
||||
size := int64(len(chunk))
|
||||
if remaining < size {
|
||||
size = remaining
|
||||
}
|
||||
written, err := w.Write(chunk[:size])
|
||||
require.NoError(t, err)
|
||||
remaining -= int64(written)
|
||||
}
|
||||
require.NoError(t, w.Close())
|
||||
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
func Test_parseMtree_boundsDecompressedSize(t *testing.T) {
|
||||
t.Run("rejects a listing past the cap", func(t *testing.T) {
|
||||
r := gzipOfSize(t, maxMtreeSize+1)
|
||||
|
||||
_, err := parseMtree(r)
|
||||
|
||||
require.ErrorContains(t, err, "larger than the max allowed size")
|
||||
})
|
||||
|
||||
t.Run("a listing at the cap is not rejected on size", func(t *testing.T) {
|
||||
// guards the off-by-one: at exactly the cap the size check must not fire, so whatever
|
||||
// happens next is the mtree parser's business and not ours
|
||||
r := gzipOfSize(t, maxMtreeSize)
|
||||
|
||||
_, err := parseMtree(r)
|
||||
|
||||
if err != nil {
|
||||
require.NotContains(t, err.Error(), "larger than the max allowed size")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user