{"id":"GHSA-8r62-w5wh-fc5m","summary":"Mailpit: WebSocket origin check bypass via percent-encoded path (regression of CVE-2026-22689)","details":"## Summary\n\nThe 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.\n\n## The defect\n\nTwo halves that were each correct in isolation. server/websockets/client.go accepts any origin and delegates the check elsewhere:\n\n```go\nvar upgrader = websocket.Upgrader{                       // line 33\n    ...\n    CheckOrigin: func(_ *http.Request) bool {            // line 37\n        // origin is checked via server.go's CORS settings\n        return true                                      // line 39\n    },\n}\n```\n\nserver/server.go performs that check but keys it on the RAW request target:\n\n```go\nif strings.HasPrefix(r.RequestURI, config.Webroot+\"api/\") || htmlPreviewRouteRe.MatchString(r.RequestURI) {   // line 320\n    if allowed := corsOriginAccessControl(r); !allowed {\n        http.Error(w, \"Blocked due to CORS violation\", http.StatusForbidden)\n        return\n    }\n```\n\nr.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.\n\nMeasured, 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.\n\nRegression 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.\n\n## Attacker model and verification\n\nAny 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.\n\n## Suggested fix\n\nDo 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.\n\n## Tooling\n\nI 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.","aliases":["CVE-2026-67448","GO-2026-6271"],"modified":"2026-09-02T23:45:38.868205369Z","published":"2026-08-20T21:34:58Z","database_specific":{"nvd_published_at":"2026-08-20T22:17:56Z","cwe_ids":["CWE-177","CWE-200","CWE-346"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-08-20T21:34:58Z"},"references":[{"type":"WEB","url":"https://github.com/axllent/mailpit/security/advisories/GHSA-8r62-w5wh-fc5m"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67448"},{"type":"WEB","url":"https://github.com/axllent/mailpit/commit/fbe5e006c3f1682b819df58b4a932d7a84920be9"},{"type":"PACKAGE","url":"https://github.com/axllent/mailpit"},{"type":"WEB","url":"https://github.com/axllent/mailpit/releases/tag/v1.30.6"}],"affected":[{"package":{"name":"github.com/axllent/mailpit","ecosystem":"Go","purl":"pkg:golang/github.com/axllent/mailpit"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.29.0"},{"fixed":"1.30.6"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 1.30.5","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-8r62-w5wh-fc5m/GHSA-8r62-w5wh-fc5m.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N"}]}