mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-13 06:09:21 -04:00
The imaging build decodes the full pixel buffer from the header-declared dimensions before the existing MaxInputWidth/MaxInputHeight guard runs, so a tiny crafted file whose header declares huge dimensions forces a multi-GB allocation and can OOM the worker. Read the header with DecodeConfig and reject oversized sources before the decode allocates, in both the imaging and vips builds, and thread the limit through the audio cover-art and geogebra decoders that decode a second attacker-controlled image.
106 lines
3.7 KiB
Go
106 lines
3.7 KiB
Go
//go:build !enable_vips
|
|
|
|
package preprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"image"
|
|
"image/jpeg"
|
|
|
|
. "github.com/onsi/ginkgo"
|
|
. "github.com/onsi/gomega"
|
|
|
|
thumbnailerErrors "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/errors"
|
|
)
|
|
|
|
// craftDimensionBomb encodes a tiny valid grayscale JPEG, then overwrites the
|
|
// SOF0 width/height so the header declares huge dimensions while the payload
|
|
// stays tiny.
|
|
func craftDimensionBomb(width, height uint16) []byte {
|
|
var buf bytes.Buffer
|
|
Expect(jpeg.Encode(&buf, image.NewGray(image.Rect(0, 0, 8, 8)), &jpeg.Options{Quality: 10})).To(Succeed())
|
|
b := buf.Bytes()
|
|
for i := 0; i+9 < len(b); i++ {
|
|
if b[i] == 0xff && b[i+1] == 0xc0 {
|
|
b[i+5], b[i+6] = byte(height>>8), byte(height)
|
|
b[i+7], b[i+8] = byte(width>>8), byte(width)
|
|
return b
|
|
}
|
|
}
|
|
Fail("no SOF0 marker in encoded jpeg")
|
|
return nil
|
|
}
|
|
|
|
var _ = Describe("ImageDecoder dimension guard", func() {
|
|
It("rejects a source whose declared dimensions exceed the limit before decoding", func() {
|
|
dec := ImageDecoder{limit: decodeLimit{maxWidth: 7680, maxHeight: 7680}}
|
|
_, err := dec.Convert(bytes.NewReader(craftDimensionBomb(20000, 20000)))
|
|
Expect(err).To(MatchError(thumbnailerErrors.ErrImageTooLarge))
|
|
})
|
|
|
|
It("decodes an image within the limit", func() {
|
|
var buf bytes.Buffer
|
|
Expect(jpeg.Encode(&buf, image.NewGray(image.Rect(0, 0, 800, 600)), nil)).To(Succeed())
|
|
dec := ImageDecoder{limit: decodeLimit{maxWidth: 7680, maxHeight: 7680}}
|
|
img, err := dec.Convert(bytes.NewReader(buf.Bytes()))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(img).ToNot(BeNil())
|
|
})
|
|
|
|
It("applies no limit when the bounds are zero", func() {
|
|
dec := ImageDecoder{}
|
|
var buf bytes.Buffer
|
|
Expect(jpeg.Encode(&buf, image.NewGray(image.Rect(0, 0, 16, 16)), nil)).To(Succeed())
|
|
_, err := dec.Convert(bytes.NewReader(buf.Bytes()))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("rejects an oversized gif before decoding", func() {
|
|
dec := GifDecoder{limit: decodeLimit{maxWidth: 7680, maxHeight: 7680}}
|
|
// LSD declares a 20000x20000 logical screen
|
|
g := []byte("GIF89a")
|
|
g = append(g, 0x20, 0x4e, 0x20, 0x4e, 0xf0, 0x00, 0x00) // 20000x20000, gct flag
|
|
g = append(g, bytes.Repeat([]byte{0}, 6)...) // minimal gct + terminator-ish
|
|
_, err := dec.Convert(bytes.NewReader(g))
|
|
Expect(err).To(MatchError(thumbnailerErrors.ErrImageTooLarge))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("dimension limit propagation", func() {
|
|
limit := decodeLimit{maxWidth: 7680, maxHeight: 7680}
|
|
opts := map[string]any{"maxInputWidth": 7680, "maxInputHeight": 7680}
|
|
|
|
It("threads the limit into decoders that recurse into ForType", func() {
|
|
Expect(ForType("audio/mpeg", opts)).To(Equal(AudioDecoder{limit: limit}))
|
|
Expect(ForType("application/vnd.geogebra.pinboard", opts)).To(Equal(GgpDecoder{limit: limit}))
|
|
g, ok := ForType("application/vnd.geogebra.slides", opts).(GgsDecoder)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(g.limit).To(Equal(limit))
|
|
})
|
|
|
|
It("rejects an oversized cover image embedded in a ggp file", func() {
|
|
bomb := craftDimensionBomb(20000, 20000)
|
|
payload := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bomb)
|
|
ggp := GGPStruct{}
|
|
ggp.Sections = append(ggp.Sections, struct {
|
|
Cards []struct {
|
|
Element struct {
|
|
Image struct{ Base64Image string }
|
|
}
|
|
}
|
|
}{Cards: []struct {
|
|
Element struct {
|
|
Image struct{ Base64Image string }
|
|
}
|
|
}{{Element: struct {
|
|
Image struct{ Base64Image string }
|
|
}{Image: struct{ Base64Image string }{Base64Image: payload}}}}})
|
|
raw, err := json.Marshal(ggp)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
_, err = GgpDecoder{limit: limit}.Convert(bytes.NewReader(raw))
|
|
Expect(err).To(MatchError(thumbnailerErrors.ErrImageTooLarge))
|
|
})
|
|
})
|