GitHub Advisoryadvisory · Go
GHSA-28pq-6qxg-wg5r Mailpit: Sibling-endpoint memory-exhaustion DoS via unbounded JSON body on /api/v1/messages, /api/v1/tags, and /api/v1/message/{id}/release (incomplete fix of GHSA-fpxj-m5q8-fphw)
MEDIUM
Official page ↗
- Aliases
- CVE-2026-48824
- Ecosystem
- Go
- Packages
- github.com/axllent/mailpit
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
- Weaknesses
- CWE-770
- Reviewed
- 2026-07-01
Affected packages
• Go github.com/axllent/mailpit: < 1.30.1 → fixed in 1.30.1
Description
Summary
The fix for GHSA-fpxj-m5q8-fphw (CVE-2026-45710, "Mailpit: Set a default 50MB p/m limit to prevent DoS via unlimited SMTP DATA and /api/v1/send body sizes") wrapped only `POST /api/v1/send` with `http.MaxBytesReader`. The four other Mailpit JSON-body API endpoints `PUT /api/v1/messages` (SetReadStatus), `DELETE /api/v1/messages` (DeleteMessages), `PUT /api/v1/tags` (SetMessageTags), and `POST /api/v1/message/{id}/release` (ReleaseMessage) still call `json.NewDecoder(r.Body)` directly with no body-size cap and remain reachable unauthenticated in the default `docker run axllent/mailpit:latest` deploy. An unauthenticated remote attacker can post a multi-million-element `IDs` slice and drive RSS from ~25 MiB baseline to ~450 MiB per 16 MB request body. Repeating across multiple connections accumulates the same per-request amplification per process.
Affected versions
• Mailpit at HEAD `67a7ca83ff759082d2b86dda07eb5bb3dad404e0` (v1.30.0, 2026-05-14).
• All versions ` 0 {
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxMessageSize)*1024*1024)
}
decoder := json.NewDecoder(r.Body)
```
The sibling JSON-body handlers were not updated. Side-by-side at HEAD `67a7ca8`:
| File | Function | `MaxBytesReader`? | Unauth in default deploy? |
|---|---|---|---|
| `server/apiv1/send.go:45-48` (`SendMessageHandler`) | POST `/api/v1/send` | YES (50 MB) | YES (via `sendAPIAuthMiddleware` falling back to `middleWareFunc`) |
| `server/apiv1/messages.go:107` (`SetReadStatus`) | PUT `/api/v1/messages` | NO | YES |
| `server/apiv1/messages.go:187` (`DeleteMessages`) | DELETE `/api/v1/messages` | NO | YES |
| `server/apiv1/tags.go:54` (`SetMessageTags`) | PUT `/api/v1/tags` | NO | YES |
| `server/apiv1/release.go:55` (`ReleaseMessage`) | POST `/api/v1/message/{id}/release` | NO | YES |
The four sibling handlers all share the shape:
```go
// server/apiv1/messages.go:107-115 (SetReadStatus)
decoder := json.NewDecoder(r.Body)
var data struct {
Read bool
IDs []string
Search string
}
err := decoder.Decode(&data)
```
No `MaxBytesReader`, no body-size cap, no `r.Header.Get("Content-Length")` check. The `json.NewDecoder` streams the body but each `"x"` element materialises as a separate Go `string` plus slice-header overhead, so the unmarshalled `[]string` slice for `IDs` grows roughly linearly with attacker payload size.
Vulnerable code
`server/apiv1/messages.go:107`:
```go
func SetReadStatus(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var data struct {
Read bool
IDs []string
Search string
}
err := decoder.Decode(&data)
if err != nil {
httpError(w, err.Error())
return
}
// ...
```
Three other handlers (`DeleteMessages`, `SetMessageTags`, `ReleaseMessage`) match the same shape.
Reachability chain (default deploy)
```
Listen() # config/config.go HTTPListen = "[::]:8025"
↓
HTTP server # server/server.go:177-186
↓
middleWareFunc(apiv1.SetReadStatus) # server/server.go:178 — auth bypassed when UICredentials == nil
↓
SetReadStatus # server/apiv1/messages.go:87
↓
json.NewDecoder(r.Body).Decode(&data) # no MaxBytesReader; allocates 4M Go strings + slice for {"IDs":["x",...]}
↓
RSS grows ~28x relative to payload size
```
`config/config.go`'s `MaxMessageSize` field (added by 136bdde) exists and is parsed from `--max-message-size` (default 50 MB), but it is checked only in `server/apiv1/send.go`. The four sibling handlers never consult it.
Reproduction (E2E against `axllent/mailpit:latest` v1.30.0)
```bash
1) start mailpit with defaults (no --ui-auth, no --smtp-auth)
docker run --name mailpit-test -d -p 18025:8025 axllent/mailpit:latest
2) baseline RSS
docker stats mailpit-test --no-stream --format '{{.MemUsage}}'
→ 8.473MiB / 5.772GiB
3) trigger
python3 - 0 {
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxMessageSize)*1024*1024)
}
decoder := json.NewDecoder(r.Body)
// server/apiv1/messages.go:187 (DeleteMessages) — same wrap
// server/apiv1/tags.go:54 (SetMessageTags) — same wrap
// server/apiv1/release.go:55 (ReleaseMessage) — same wrap
```
A cleaner shape is to factor the cap into the existing `middleWareFunc` wrapper in `server/server.go`, so every API handler that is not an upload-style endpoint inherits the cap by default.
Credit
Reported by tonghuaroot.
Related
Alias · CVE (1)
- CVE-2026-48824 Mailpit is an email testing tool and API for developers. vulnerability
Weakness · CWE (1)
- CWE-770 Allocation of Resources Without Limits or Throttling weakness