diff --git a/README.md b/README.md index 8200596..d1ab548 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,10 @@ path under `/v1/` answers 200, in maintenance mode too. redirects to, is `localhost`, ends in `.localhost` or `.local`, or has an address in a blocked network (see `blocked_networks`); 429 over `image_requests_per_minute` (below); 415 when the upstream answers with a - `Content-Type` that is not one of the source formats (below), or for `orig` of - a source pixa cannot save in its own format; 502 when the upstream answered - with an error status, and for 5 minutes after that for the same source URL; - 503 when pixa is busy or in maintenance mode; 500 for any other failure. + `Content-Type` that is not one of the source formats (below), or with an image + libvips would not load as one of them; 502 when the upstream answered with an + error status, and for 5 minutes after that for the same source URL; 503 when + pixa is busy or in maintenance mode; 500 for any other failure. - `GET` or `HEAD` `/v1/e//` — an image through an encrypted URL (see Encrypted URLs). Needs: nothing but the URL. Answers: 200; 304 when `If-None-Match` matches the image's `ETag`; 400 for a token that does not @@ -265,7 +265,8 @@ once, is refused with 400. The source image may be JPEG, PNG, GIF, WebP, AVIF or JPEG XL. The upstream's `Content-Type` must name its format, and the image's first bytes must match it. An upstream answering with any other `Content-Type`, SVG included, is refused -with 415. +with 415, and so is an image that libvips would not load as one of these +formats, such as an SVG whose first bytes pass the check for AVIF. With the format `auto`, pixa chooses the format for each request from its `Accept` header, in this order: diff --git a/TODO.md b/TODO.md index 1a104cf..6cd9a0e 100644 --- a/TODO.md +++ b/TODO.md @@ -34,12 +34,16 @@ P2: security: per-origin rate limiting of the source's own format (closes #68): the upstream fetch no longer accepts `image/svg+xml`, which libvips would render. Both image routes answer 415 for an upstream `Content-Type` that is not one of the source formats, SVG - included, where they answered 500. `orig` of a source in a format the image - processor cannot name is `ErrUnsupportedInputFormat`, also answered 415, where - it was saved as JPEG. `README.md` no longer lists SVG. The SVG detection in + included, where they answered 500. Before decoding, the image processor + refuses with `ErrUnsupportedInputFormat`, also answered 415, any input that + govips' `DetermineImageType`, which picks the libvips loader, does not name as + a source format. That catches an SVG whose first bytes pass the first-bytes + check for another type, as an XML comment can make them do for AVIF; an SVG + whose first bytes do not pass that check answers 500, as before. `orig` is + always the source's own format, where a source the processor could not name + was saved as JPEG. `README.md` no longer lists SVG. The SVG detection in `internal/magic` stays: the request path does not call it, and it goes with - the unused code of https://git.eeqj.de/sneak/pixa/issues/168. An SVG sent - under another image type still fails the first-bytes check and answers 500. + the unused code of https://git.eeqj.de/sneak/pixa/issues/168. - 2026-10-12 per-client limit on the image routes (closes #236): the new setting `image_requests_per_minute` (`PIXA_IMAGE_REQUESTS_PER_MINUTE`), default 600, is how many `GET` and `HEAD` requests one client may send to `/v1/image/` and diff --git a/internal/imageprocessor/imageprocessor.go b/internal/imageprocessor/imageprocessor.go index af58690..1898ce7 100644 --- a/internal/imageprocessor/imageprocessor.go +++ b/internal/imageprocessor/imageprocessor.go @@ -137,9 +137,8 @@ var ErrInputDataTooLarge = errors.New("input data exceeds maximum allowed size") // not supported. var ErrUnsupportedOutputFormat = errors.New("unsupported output format") -// ErrUnsupportedInputFormat is returned when the requested format is orig and -// the input is in a format the processor cannot name, so it cannot be saved -// in that format. +// ErrUnsupportedInputFormat is returned, before the input is decoded, when +// libvips would not load it as one of the source formats. var ErrUnsupportedInputFormat = errors.New("unsupported input format") // ErrTooManyImages is returned when MaxConcurrentProcessing images are being @@ -224,6 +223,16 @@ func (p *ImageProcessor) Process( return nil, ErrInputDataTooLarge } + // govips picks the libvips loader with DetermineImageType. Input it + // would not load as a source format, such as SVG, which librsvg would + // render, is refused before libvips reads it. + inputFormat := p.detectFormat(vips.DetermineImageType(data)) + + sourceFormat, err := p.formatFromString(inputFormat) + if err != nil { + return nil, err + } + // Decode image img, err := vips.NewImageFromBuffer(data) if err != nil { @@ -242,9 +251,6 @@ func (p *ImageProcessor) Process( origWidth := img.Width() origHeight := img.Height() - // Detect input format - inputFormat := p.detectFormat(img) - // Validate input dimensions to prevent DoS via decompression bombs if origWidth > MaxInputDimension || origHeight > MaxInputDimension { return nil, ErrInputTooLarge @@ -261,14 +267,10 @@ func (p *ImageProcessor) Process( } } - // orig is the source's own format, and an error when the processor - // cannot name it; encode refuses an empty format + // orig is the source's own format; encode refuses an empty format outputFormat := req.Format if outputFormat == FormatOriginal { - outputFormat, err = p.formatFromString(inputFormat) - if err != nil { - return nil, err - } + outputFormat = sourceFormat } // Encode to target format @@ -412,10 +414,9 @@ func (p *ImageProcessor) acquireSlot(ctx context.Context) (func(), error) { } } -// detectFormat returns the format string from a vips image. -func (p *ImageProcessor) detectFormat(img *vips.ImageRef) string { - format := img.Format() - switch format { +// detectFormat returns the format string for a govips image type. +func (p *ImageProcessor) detectFormat(imageType vips.ImageType) string { + switch imageType { case vips.ImageTypeJPEG: return "jpeg" case vips.ImageTypePNG: