Documentation
¶
Overview ¶
Package imgprobe inspects image bytes using only pure-Go decoders (no cgo): it sniffs the real format from magic bytes, reads dimensions cheaply (header only, never a full decode), applies EXIF orientation, and rejects decompression bombs via a total-pixel cap BEFORE any pixels are allocated.
Why ¶
Reading an image's dimensions by fully decoding it is a denial-of-service vector: a 15000×15000 PNG is a tiny file on disk but decodes to ~900 MB of pixels. imgprobe reads only the header via each format's DecodeConfig and rejects the image when its DECLARED dimensions exceed a total-area cap, so the bomb is refused before anything is allocated.
Usage ¶
res, err := imgprobe.Config(data)
switch {
case errors.Is(err, imgprobe.ErrTooLarge): // → 413 Payload Too Large
case errors.Is(err, imgprobe.ErrUnsupported): // → 415 Unsupported Media Type
case err != nil:
default:
// res.Format == "jpeg", res.Width, res.Height ...
}
The enabled format set is an allowlist (default {JPEG, PNG, WEBP}); pass WithFormats to change it, WithOrientation to supply the EXIF orientation, and WithMaxPixels/WithMaxSide to tune the caps.
Security ¶
imgprobe protects the "read dimensions" step and nothing more:
- It reads only headers (DecodeConfig), so a hostile image cannot force a full-frame allocation here.
- The total-area cap (WithMaxPixels, default 50 MP) is the real bomb guard; a per-side cap alone is insufficient because a square image passes it.
- Format detection is by magic bytes, ignoring the claimed Content-Type or file extension.
It is NOT a substitute for bounding the raw upload size before you buffer the bytes, and any later full decode (e.g. resizing) must apply its own limits.
Index ¶
Examples ¶
Constants ¶
const ( DefaultMaxPixels = 50_000_000 // 50 MP, matching real camera output DefaultMaxSide = 15000 )
Default caps. MaxSide is a per-side sanity bound; MaxPixels is the total-area cap that actually guards against decompression bombs — a 15000x15000 image passes a per-side check but decodes to ~900 MB.
Variables ¶
var ( // ErrUnsupported means the bytes are not one of the enabled formats, or the // header could not be decoded. Maps naturally to 415 Unsupported Media Type. ErrUnsupported = errors.New("imgprobe: unsupported or invalid image") // ErrTooLarge means the declared dimensions exceed the configured caps. Maps // naturally to 413 Payload Too Large. ErrTooLarge = errors.New("imgprobe: image exceeds size limits") )
Sentinels — HTTP handlers can map these to distinct status codes (415 vs 413). Both are wrapped in the returned error, so errors.Is works.
var ( JPEG = Format{"jpeg", "image/jpeg", prefixMatcher([]byte{0xFF, 0xD8, 0xFF}), decodeVia(jpeg.DecodeConfig)} PNG = Format{"png", "image/png", prefixMatcher([]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}), decodeVia(png.DecodeConfig)} WEBP = Format{"webp", "image/webp", matchWebP, decodeVia(webp.DecodeConfig)} GIF = Format{"gif", "image/gif", matchGIF, decodeVia(gif.DecodeConfig)} BMP = Format{"bmp", "image/bmp", prefixMatcher([]byte("BM")), decodeVia(bmp.DecodeConfig)} TIFF = Format{"tiff", "image/tiff", matchTIFF, decodeVia(tiff.DecodeConfig)} )
Built-in format descriptors. JPEG, PNG and WEBP are enabled by default; GIF, BMP and TIFF are opt-in via WithFormats.
Functions ¶
Types ¶
type DecodeConfigFunc ¶
DecodeConfigFunc reads image dimensions from a header without a full decode.
type Format ¶
type Format struct {
// contains filtered or unexported fields
}
Format describes a recognized image format: its name, content type, magic-byte matcher, and pure-Go header decoder. Config dispatches on each enabled format's own decoder rather than Go's global image registry, so enabling is truly per-call: a disabled format is ErrUnsupported even though its decoder is linked into the binary.
func NewFormat ¶
func NewFormat(name, contentType string, magic []byte, dc DecodeConfigFunc) Format
NewFormat registers a custom format. magic is matched against the leading bytes of the input; dc reads its dimensions.
func (Format) ContentType ¶
ContentType returns the canonical MIME type (e.g. "image/jpeg").
type Option ¶
type Option func(*config)
Option configures Sniff and Config. Safe defaults apply when omitted.
func WithFormats ¶
WithFormats sets the EXACT enabled set (an allowlist), replacing the default {JPEG, PNG, WEBP}. "Defaults plus GIF" is spelled explicitly: WithFormats(imgprobe.JPEG, imgprobe.PNG, imgprobe.WEBP, imgprobe.GIF). An empty argument list is ignored (the default set stays in effect).
Example ¶
Accept an explicit allowlist that adds GIF to the defaults.
gif := mustGIF(16, 16)
res, err := imgprobe.Config(gif, imgprobe.WithFormats(
imgprobe.JPEG, imgprobe.PNG, imgprobe.WEBP, imgprobe.GIF,
))
if err != nil {
fmt.Println("rejected:", err)
return
}
fmt.Printf("%s %dx%d\n", res.Format, res.Width, res.Height)
Output: gif 16x16
func WithMaxPixels ¶
WithMaxPixels overrides the total-area cap (default DefaultMaxPixels). Non-positive values are ignored.
func WithMaxSide ¶
WithMaxSide overrides the per-side cap (default DefaultMaxSide). Non-positive values are ignored.
func WithOrientation ¶
WithOrientation supplies the EXIF orientation (1..8); values 5..8 swap width and height. The default is 1 (no adjustment). imgprobe does not parse EXIF — the caller extracts orientation separately.
type Result ¶
type Result struct {
Format string
ContentType string
Width int // post-orientation
Height int // post-orientation
}
Result is what a probe reveals about the bytes. Format is a neutral name string ("jpeg", "png", "webp", "gif", "bmp", "tiff").
func Config ¶
Config reads format + dimensions cheaply (header only, never a full decode), applies the EXIF orientation, and enforces the caps. It returns ErrTooLarge for a cap violation and ErrUnsupported for a bad or disabled format.
Example ¶
Probe an upload's header: format and post-orientation dimensions, without decoding the full image.
data := mustPNG(640, 480) // stand-in for the uploaded bytes
res, err := imgprobe.Config(data)
if err != nil {
fmt.Println("rejected:", err)
return
}
fmt.Printf("%s %dx%d\n", res.Format, res.Width, res.Height)
Output: png 640x480
Example (DecompressionBomb) ¶
Reject a decompression bomb before allocating any pixels: only the header is read, and the declared dimensions blow the total-area cap.
bomb := mustPNG(8000, 7000) // 56 MP > the 50 MP default cap _, err := imgprobe.Config(bomb) fmt.Println(errors.Is(err, imgprobe.ErrTooLarge))
Output: true