bound imageprocessor.Process input read to prevent unbounded memory use
ImageProcessor.Process used io.ReadAll without a size limit, allowing arbitrarily large inputs to exhaust memory. Add a configurable maxInputBytes limit (default 50 MiB, matching the fetcher limit) and reject inputs that exceed it with ErrInputDataTooLarge. Also bound the cached source content read in the service layer to prevent unexpectedly large cached files from consuming unbounded memory. Extracted loadCachedSource helper to reduce nesting complexity.
This commit is contained in:
@@ -26,20 +26,36 @@ func initVips() {
|
||||
// Images larger than this are rejected to prevent DoS via decompression bombs.
|
||||
const MaxInputDimension = 8192
|
||||
|
||||
// DefaultMaxInputBytes is the default maximum input size in bytes (50 MiB).
|
||||
// This matches the default upstream fetcher limit.
|
||||
const DefaultMaxInputBytes = 50 << 20
|
||||
|
||||
// ErrInputTooLarge is returned when input image dimensions exceed MaxInputDimension.
|
||||
var ErrInputTooLarge = errors.New("input image dimensions exceed maximum")
|
||||
|
||||
// ErrInputDataTooLarge is returned when the raw input data exceeds the configured byte limit.
|
||||
var ErrInputDataTooLarge = errors.New("input data exceeds maximum allowed size")
|
||||
|
||||
// ErrUnsupportedOutputFormat is returned when the requested output format is not supported.
|
||||
var ErrUnsupportedOutputFormat = errors.New("unsupported output format")
|
||||
|
||||
// ImageProcessor implements the Processor interface using libvips via govips.
|
||||
type ImageProcessor struct{}
|
||||
type ImageProcessor struct {
|
||||
maxInputBytes int64
|
||||
}
|
||||
|
||||
// NewImageProcessor creates a new image processor.
|
||||
func NewImageProcessor() *ImageProcessor {
|
||||
// NewImageProcessor creates a new image processor with the given maximum input
|
||||
// size in bytes. If maxInputBytes is <= 0, DefaultMaxInputBytes is used.
|
||||
func NewImageProcessor(maxInputBytes int64) *ImageProcessor {
|
||||
initVips()
|
||||
|
||||
return &ImageProcessor{}
|
||||
if maxInputBytes <= 0 {
|
||||
maxInputBytes = DefaultMaxInputBytes
|
||||
}
|
||||
|
||||
return &ImageProcessor{
|
||||
maxInputBytes: maxInputBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// Process transforms an image according to the request.
|
||||
@@ -48,12 +64,20 @@ func (p *ImageProcessor) Process(
|
||||
input io.Reader,
|
||||
req *ImageRequest,
|
||||
) (*ProcessResult, error) {
|
||||
// Read input
|
||||
data, err := io.ReadAll(input)
|
||||
// Read input with a size limit to prevent unbounded memory consumption.
|
||||
// We read at most maxInputBytes+1 so we can detect if the input exceeds
|
||||
// the limit without consuming additional memory.
|
||||
limited := io.LimitReader(input, p.maxInputBytes+1)
|
||||
|
||||
data, err := io.ReadAll(limited)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
if int64(len(data)) > p.maxInputBytes {
|
||||
return nil, ErrInputDataTooLarge
|
||||
}
|
||||
|
||||
// Decode image
|
||||
img, err := vips.NewImageFromBuffer(data)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user