mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-12 21:58:58 -04:00
fix(thumbnails): harden the raw tiff walker against crafted files
Cap the IFD queue at maxIFDs so a file packed with huge SubIFD counts can no longer grow it without bound, and walk the embedded JPEG by a bounded number of header segments so a preview whose SOF sits past 64KB of leading metadata is still found instead of being scanned only within a fixed byte window.
This commit is contained in:
1 parent
77b08abfb6
commit
61ce147205
2 files changed
+81
-11
No files matched your search
@@ -42,9 +42,10 @@ const (
|
||||
tiffTagJPEGLength = 0x0202 // JPEGInterchangeFormatLength
|
||||
tiffTypeShort = 3
|
||||
tiffTypeLong = 4
|
||||
// cycle and decompression-bomb guard for untrusted IFD chains
|
||||
maxIFDs = 64
|
||||
sofScanLimit = 64 * 1024
|
||||
// bound work on untrusted input: cap processed IFDs (and the queue) and
|
||||
// the JPEG header segments walked before the SOF marker
|
||||
maxIFDs = 64
|
||||
maxJPEGSegments = 32
|
||||
)
|
||||
|
||||
// extractEmbeddedJPEG walks the IFD chain incl. SubIFDs and returns the
|
||||
@@ -72,6 +73,13 @@ func extractEmbeddedJPEG(data []byte) ([]byte, uint16, error) {
|
||||
|
||||
queue := []uint32{order.Uint32(data[4:8])}
|
||||
seen := map[uint32]struct{}{}
|
||||
// push queues an IFD offset unless the queue is already full; a crafted
|
||||
// file with millions of SubIFD pointers must not grow it without bound
|
||||
push := func(offset uint32) {
|
||||
if len(queue) < maxIFDs {
|
||||
queue = append(queue, offset)
|
||||
}
|
||||
}
|
||||
first := true
|
||||
for len(queue) > 0 && len(seen) < maxIFDs {
|
||||
ifdOffset := queue[0]
|
||||
@@ -123,16 +131,16 @@ func extractEmbeddedJPEG(data []byte) ([]byte, uint16, error) {
|
||||
continue
|
||||
}
|
||||
if count == 1 {
|
||||
queue = append(queue, order.Uint32(entry[8:12]))
|
||||
push(order.Uint32(entry[8:12]))
|
||||
continue
|
||||
}
|
||||
arrayOffset := order.Uint32(entry[8:12])
|
||||
for j := uint32(0); j < count && j < maxIFDs; j++ {
|
||||
pos := int(arrayOffset) + int(j)*4
|
||||
if pos+4 > len(data) {
|
||||
if pos+4 > len(data) || len(queue) >= maxIFDs {
|
||||
break
|
||||
}
|
||||
queue = append(queue, order.Uint32(data[pos:pos+4]))
|
||||
push(order.Uint32(data[pos : pos+4]))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +150,7 @@ func extractEmbeddedJPEG(data []byte) ([]byte, uint16, error) {
|
||||
if stripOffset > 0 && stripLength > 0 {
|
||||
candidates = append(candidates, candidate{stripOffset, stripLength})
|
||||
}
|
||||
queue = append(queue, order.Uint32(data[entriesEnd:entriesEnd+4]))
|
||||
push(order.Uint32(data[entriesEnd : entriesEnd+4]))
|
||||
first = false
|
||||
}
|
||||
|
||||
@@ -170,11 +178,8 @@ func isRenderableJPEG(buf []byte) bool {
|
||||
if len(buf) < 4 || buf[0] != 0xff || buf[1] != 0xd8 {
|
||||
return false
|
||||
}
|
||||
if len(buf) > sofScanLimit {
|
||||
buf = buf[:sofScanLimit]
|
||||
}
|
||||
i := 2
|
||||
for i+4 <= len(buf) {
|
||||
for segments := 0; i+4 <= len(buf) && segments < maxJPEGSegments; segments++ {
|
||||
if buf[i] != 0xff {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -156,4 +157,68 @@ var _ = Describe("RawTiffDecoder", func() {
|
||||
Expect(bounds.Dy()).To(Equal(64))
|
||||
})
|
||||
})
|
||||
Describe("hardening against crafted files", func() {
|
||||
It("stays bounded when an IFD is packed with huge SubIFD counts", func() {
|
||||
le := binary.LittleEndian
|
||||
preview := encodeJPEG(64, 32)
|
||||
entry := func(tag, typ uint16, count, value uint32) []byte {
|
||||
e := le.AppendUint16(nil, tag)
|
||||
e = le.AppendUint16(e, typ)
|
||||
e = le.AppendUint32(e, count)
|
||||
return le.AppendUint32(e, value)
|
||||
}
|
||||
const subEntries = 200
|
||||
buf := []byte{'I', 'I', 42, 0, 8, 0, 0, 0}
|
||||
ifdStart := uint32(len(buf))
|
||||
entryCount := uint16(2 + subEntries)
|
||||
previewStart := ifdStart + 2 + uint32(entryCount)*12 + 4
|
||||
|
||||
ifd := le.AppendUint16(nil, entryCount)
|
||||
ifd = append(ifd, entry(tiffTagJPEGOffset, tiffTypeLong, 1, previewStart)...)
|
||||
ifd = append(ifd, entry(tiffTagJPEGLength, tiffTypeLong, 1, uint32(len(preview)))...)
|
||||
for i := 0; i < subEntries; i++ {
|
||||
// each claims 4 billion SubIFD pointers at a bogus array offset
|
||||
ifd = append(ifd, entry(tiffTagSubIFDs, tiffTypeLong, 0xffffffff, 8)...)
|
||||
}
|
||||
ifd = le.AppendUint32(ifd, 0)
|
||||
buf = append(buf, ifd...)
|
||||
buf = append(buf, preview...)
|
||||
|
||||
done := make(chan []byte, 1)
|
||||
go func() {
|
||||
jpg, _, _ := extractEmbeddedJPEG(buf)
|
||||
done <- jpg
|
||||
}()
|
||||
select {
|
||||
case jpg := <-done:
|
||||
Expect(jpg).To(Equal(preview))
|
||||
case <-time.After(5 * time.Second):
|
||||
Fail("extractEmbeddedJPEG did not return within 5s on a crafted file")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Describe("isRenderableJPEG", func() {
|
||||
It("reaches a SOF that lies past 64KB of leading segments", func() {
|
||||
buf := []byte{0xff, 0xd8}
|
||||
// two ~40KB APP1 segments push the SOF past the old 64KB window
|
||||
for n := 0; n < 2; n++ {
|
||||
const payload = 40000
|
||||
buf = append(buf, 0xff, 0xe1, byte((payload+2)>>8), byte((payload+2)&0xff))
|
||||
buf = append(buf, make([]byte, payload)...)
|
||||
}
|
||||
buf = append(buf, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0, 16, 0, 16, 0x01, 0x01, 0x11, 0x00)
|
||||
Expect(isRenderableJPEG(buf)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("rejects a stream that hides the SOF behind too many segments", func() {
|
||||
buf := []byte{0xff, 0xd8}
|
||||
for n := 0; n < maxJPEGSegments+5; n++ {
|
||||
buf = append(buf, 0xff, 0xe1, 0x00, 0x04, 0x00, 0x00) // tiny APP1
|
||||
}
|
||||
buf = append(buf, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0, 16, 0, 16, 0x01, 0x01, 0x11, 0x00)
|
||||
Expect(isRenderableJPEG(buf)).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
Reference in new issue
Block a user