GitHub Advisoryadvisory · Go
GHSA-75mr-qw9x-3r39 Mailpit: Thumbnail generation decodes unbounded image dimensions before scaling
HIGH
Official page ↗
- Aliases
- CVE-2026-67446
- Ecosystem
- Go
- Packages
- github.com/axllent/mailpit
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-400, CWE-770
- Reviewed
- 2026-09-02
Affected packages
• Go github.com/axllent/mailpit: < 1.30.4 → fixed in 1.30.4
Description
Summary
Mailpit's thumbnail endpoint decodes attacker-supplied image attachments into a full raster before checking any decoded-pixel, dimension, or memory budget. A remote client that can store an email and reach the default web API can supply a compact high-dimension image, then request `/api/v1/message/{id}/part/{partID}/thumb` to force server-side memory and CPU work far larger than the encoded attachment size before Mailpit returns a 180x120 thumbnail.
Technical Details
The route is registered as `GET /api/v1/message/{id}/part/{partID}/thumb` in `server/server.go`. The handler in `server/apiv1/thumbnails.go` loads the requested attachment and accepts any part whose content type begins with `image/`:
```go
a, err := storage.GetAttachmentPart(id, partID)
// ...
if !strings.HasPrefix(a.ContentType, "image/") {
blankImage(a, w)
return
}
buf := bytes.NewBuffer(a.Content)
img, err := imaging.Decode(buf, imaging.AutoOrientation(true))
```
`storage.GetAttachmentPart()` reparses the stored raw email and returns the matching attacker-supplied attachment bytes. `Thumbnail()` then calls `imaging.Decode()` before any check on declared dimensions or estimated decoded bytes. The subsequent `imaging.Fill(img, 180, 120, ...)`, `imaging.Clone()`, and JPEG encode only happen after the full image has already been decoded.
The thumbnail output is fixed at 180x120, so the endpoint does not need to decode arbitrarily large rasters. The current implementation lets a small compressed PNG declare large dimensions and expand to tens or hundreds of MiB of decoded pixels before scaling. The default message-size controls do not stop this class: they bound encoded message/attachment bytes, while this issue is encoded-size to decoded-raster amplification after storage.
The UI also naturally reaches this endpoint for image attachments. `server/ui-src/components/message/MessageAttachments.vue` uses `/api/v1/message/{message.ID}/part/{part.PartID}/thumb` as the `` for image attachments, so opening an affected message in the web UI can trigger the decode path. A client with API access can also call the endpoint directly.
PoV
The following test creates a valid all-zero RGBA PNG by streaming compressed scanlines, so the generator does not need to allocate the full source image. It then exercises both the direct decode/scale operation and the real handler path: store an email with the PNG attachment, resolve the actual `PartID`, and call `Thumbnail()`.
The oversized case uses a 4096x4096 image. That is intentionally bounded for safe local reproduction, but it is enough to show a 65,301-byte encoded PNG becoming an estimated 67,108,864-byte decoded RGBA raster before thumbnail scaling. The negative control is a 16x16 PNG.
```go
package apiv1
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/binary"
"fmt"
"hash/crc32"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/kovidgoyal/imaging"
)
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 solidRGBApng(width, height int) []byte {
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
ihdr[9] = 6
out.Write(pngChunk("IHDR", ihdr))
var compressed bytes.Buffer
zw := zlib.NewWriter(&compressed)
row := make([]byte, 1+width*4)
for i := 0; i 76 {
lines = append(lines, encoded[:…
Related
Alias · CVE (1)
- CVE-2026-67446 Mailpit is an email testing tool and API for developers. vulnerability
References
- https://github.com/axllent/mailpit/security/advisories/GHSA-75mr-qw9x-3r39 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-67446 advisory
- https://github.com/axllent/mailpit/commit/6bcb6337838b542d53c348e38c7977f569b6db35 web
- https://github.com/axllent/mailpit package
- https://github.com/axllent/mailpit/releases/tag/v1.30.4 web