Asymmetric denial of service In github.com/axllent/mailpit
Description
Mailpit: Unauthenticated remote memory-exhaustion DoS via unlimited SMTP DATA and /api/v1/send body sizes
Summary
The Mailpit SMTP server has a Server.MaxSize int field that controls the maximum allowed DATA payload size, but the field is never assigned anywhere outside test code, leaving it at Go's zero value (0 ⇒ "no limit"). The same applies to the HTTP /api/v1/send endpoint, whose request body is decoded with json.NewDecoder(r.Body) and no http.MaxBytesReader. Because Mailpit's default listeners bind [::]:1025 (SMTP) and [::]:8025 (HTTP), with no authentication required on either, a single network-reachable attacker can push an arbitrarily large message into Mailpit and watch RAM consumption spike with a ~7-10× amplification factor (raw frame → enmime envelope tree → search-text index → zstd-encoded write to SQLite). Repeating the attack — or running it concurrently from multiple connections — drives the process to OOM-kill.
Details
Pre-auth, remote DoS on every Mailpit deployment running the default configuration. Memory is the primary axis; disk is a secondary one, because each oversized message is also persisted to the SQLite store (config.MaxMessages caps the count at 500 but never the bytes — so 500 attacker-sized messages × 1 GiB each = ~500 GiB on the host disk before the LRU rotates).
Affected code internal/smtpd/smtpd.go:107 — the field exists:
type Server struct { ... MaxSize int // Maximum message size allowed, in bytes ... }
internal/smtpd/smtpd.go:863-877 — the enforcement is gated on > 0:
for { ... line, err := s.br.ReadBytes('\n') if err != nil { return nil, err } if bytes.Equal(line, []byte(".\r\n")) { break...
internal/smtpd/main.go:223-248 — the field is never populated; grep -rn "MaxSize" cmd/ config/ returns zero hits. There is no --smtp-max-message-size CLI flag, no MP_SMTP_MAX_MESSAGE_SIZE env var.
server/apiv1/send.go:45-52 — HTTP path has the same defect:
decoder := json.NewDecoder(r.Body) data := sendMessageParams{} if err := decoder.Decode(&data.Body); err != nil { httpJSONError(w, err.Error()) return }
No r.Body = http.MaxBytesReader(w, r.Body, N) wrapper; server.ReadTimeout of 30 s is transmission-time, not body-size-budget.
PoC
Baseline RSS on a freshly-started binary: 25 MiB. After one 100 MiB SMTP DATA block: ~1 037 MiB (≈10× amplification, single connection, no auth):
#!/usr/bin/env python3 # poc-smtp-dos.py import socket, sys host, port = sys.argv[1], int(sys.argv[2]) mb = int(sys.argv[3]) # message size, MiB s = socket.create_connection((host, port), timeout=120) def r(): return s.recv(4096).decode("latin-1", "replace").strip()...
$ python3 poc-smtp-dos.py 127.0.0.1 1025 100 220 hostname Mailpit ESMTP Service ready 250 hostname greets x 250 2.1.0 Ok 250 2.1.5 Ok 354 Start mail input; end with <CR><LF>.<CR><LF> 250 2.0.0 Ok: queued as 58rI69JTJYjVFwogEbw9Jj ...
Equivalent over HTTP:
# poc-http-dos.py import socket, sys host, port, mb = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]) prefix = b'{"From":{"Email":"[email protected]"},"To":[{"Email":"[email protected]"}],"Subject":"big","Text":"' suffix = b'"}' N = mb * 1024 * 1024 clen = len(prefix) + N + len(suffix) ...
$ python3 poc-http-dos.py 127.0.0.1 8025 200 HTTP/1.1 200 OK ... $ ps -o rss= -p $(pgrep -f /usr/local/bin/mailpit) 2147000 # comfortably above 2 GiB on the same process
Five concurrent SMTP connections × 50 MiB each took the same machine from 25 MiB → 1 970 MiB during the attack window. With sufficient bandwidth the only ceiling is host RAM.
Impact
Unauthenticated remote attackers can send arbitrarily large emails via SMTP or HTTP, causing unbounded memory and disk growth, leading to out-of-memory (OOM) kills and full Mailpit process crash (DoS)
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
go | 1.30.0 |
Aliases
References