GitHub Advisoryadvisory · Go

GHSA-xqhv-chqm-fhcc Joro: Unauthenticated Cross-Origin Plugin Upload Leads to RCE

CRITICAL Published 2026-07-08 Modified 2026-07-08 Official page ↗
Aliases
CVE-2026-53649
Ecosystem
Go
Packages
github.com/BishopFox/joro
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
Weaknesses
CWE-306, CWE-352, CWE-434, CWE-942
Reviewed
2026-07-08

Affected packages

• Go github.com/BishopFox/joro: < 0.0.0-20260601151442-5c0ca35db828 → fixed in 0.0.0-20260601151442-5c0ca35db828

Description

Unauthenticated Cross-Origin Plugin Upload Leads to RCE (Joro ≤ v1.1.0) Severity: Critical CVSS v3.1: 9.6 (AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H) Affected versions: Joro ≤ v1.1.0, proxy mode (default), Linux/macOS Reporter: cstover Date: 2026-05-27 --- Summary Joro's default proxy mode (in versions <= 1.1.0) exposes a local API on `127.0.0.1:9090` that performs no authentication and applies a wildcard CORS policy. Because plugin uploads use the CORS-safelisted `multipart/form-data` content type, cross-origin JavaScript on any page the operator visits can reach privileged endpoints - including uploading a native plugin and triggering a restart - directly through the operator's browser, with no preflight or credentials. Since plugins execute on load, this yields unauthenticated remote code execution as the operator's user from a single page visit. --- Root Cause Three weaknesses combined into the exploit chain. 1. No authentication in proxy mode. `internal/api/server.go` applied `AuthMiddleware` only when `listenerMode` was `true`. In the default proxy mode every API endpoint — including plugin upload and system restart — accepted requests without any token, cookie, or credential. 2. Permissive CORS with an insufficient protection assumption. `corsMiddleware` set `Access-Control-Allow-Origin: *` unconditionally on all responses. `SECURITY.md` documented this as an intentional tradeoff on the basis that proxy mode binds to `127.0.0.1`, which the document states "limits exposure to the local machine." That assumption was incorrect. `multipart/form-data` is a CORS-safelisted `Content-Type`, so cross-origin JavaScript can POST files to the Joro API without triggering a preflight request — the browser allows it. Any web page the operator visited reached the localhost API through their browser without restriction. The localhost bind provided no protection against browser-mediated requests. 3. Plugin `init()` executed on `plugin.Open()` before symbol lookup. `internal/plugins/loader.go` called `plugin.Open()`, which ran the plugin's `init()` functions before any symbol lookup occurred. A plugin with no exports still executed its payload the moment Joro restarted. --- Attack Chain 1. The operator visits an attacker-controlled page in Firefox on their machine. 2. JavaScript on the page fetches `pwn.so` from the attacker's server (same-origin, no CORS issue). 3. JavaScript POSTs `pwn.so` to `http://127.0.0.1:9090/api/v1/plugins/upload` as `multipart/form-data`. Joro accepts it — no auth, no preflight. 4. JavaScript POSTs to `http://127.0.0.1:9090/api/v1/system/restart`. Joro re-executes. 5. On restart, `plugin.Open("pwn.so")` calls `init()`, which opens a goroutine and dials back to the attacker's listener. 6. An interactive `/bin/bash -i` shell is obtained as the operator's user. The plugin ABI matches without any access to the operator's machine. The same public v1.1.0 release tarball is downloaded and Joro's own `--build-plugin` feature is used, which reads `runtime/debug.BuildInfo` from the release binary and forwards every ABI-relevant flag. One `.so` works against every operator running that release. --- Impact Unauthenticated, remote, browser-mediated code execution as the operator's user. Because the exploit pivots through the operator's browser to the loopback-bound API, the network bind offers no protection, and a single ABI-matched plugin works against every operator running the affected release. Fix The chain is broken at multiple layers. Cross-origin browser access to the proxy-mode API is eliminated, the API is restricted to same-origin requests targeting a loopback host, and the UI/API is bound to loopback only. 1. Removed the wildcard CORS header and gated the proxy-mode API behind a same-origin guard `corsMiddleware` (which set `Access-Control-Allow-Origin: *` on every response) was deleted, and proxy mode now wraps the API in `originGuard` instead. (`internal/api/server.go`, commit `5c0ca35`) ```diff var handler http.Handler = mux if s.listenerMode { + // Listener/teamserver: bearer-token auth. handler = team.AuthMiddleware(s.teamToken, handler) +} else { + // Proxy mode: restrict the API to same-origin browser requests. + handler = originGuard(uiBind, handler) } -handler = corsMiddleware(handler) ``` ```diff -// corsMiddleware adds permissive CORS headers for dev usage. -func corsMiddleware(next http.Handler) http.Handler { • return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { • w.Header().Set("Access-Control-Allow-Origin", "*") • w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") • w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Joro-Nickname") • if r.Method == http.MethodOptions { • w.WriteHeader(http.StatusNoContent) • return • } • next.ServeHTTP(w, r) • }) -} ``` 2. Same-origin enforcement via `Sec-Fetch-Site` + `Origin`/`Host` `originGuard` rejects state-changing requests (and the `/ws` upgrade) whose `Sec-Fetch-Site` indicates a cross-origin initiator or whose `Origin` host does not match the request `Host`. Non-browser local tooling (no browser headers) is still allowed. (`internal/api/originguard.go`, commit `5c0ca35`) ```go func isMutating(method string) bool { switch method { case http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch: return true default: return false } } func sameOrigin(r *http.Request) bool { switch r.Header.Get("Sec-Fetch-Site") { case "", "same-origin", "none": // Same-origin, a direct navigation, or a non-browser client. default: // "cross-site", "same-site" return false } if origin := r.Header.Get("Origin"); origin != "" { if origin == "null" { return false // opaque/sandboxed cross-origin context } u, err := url.Parse(origin) if err != nil || !strings.EqualFold(reqHostname(u.Host), reqHostname(r.Host)) { return false } } return true } ``` 3. Tightened the WebSocket origin check The WebSocket upgrader previously accepted every origin (`CheckOrigin: return true`). It now rejects cross-origin handshakes while still permitting non-browser clients. (`internal/api/ws.go`, commit `5c0ca35`) ```diff var upgrader = websocket.Upgrader{ • CheckOrigin: func(r *http.Request) bool { return true }, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + if origin == "" { + return true + } + if origin == "null" { + return false + } + u, err := url.Parse(origin) + if err != nil { + return false + } + return strings.EqualFold(reqHostname(u.Host), reqHostname(r.Host)) + }, } ``` 4. Bound the proxy-mode UI/API to loopback and removed the wildcard host exception The same-origin check alone can be defeated by DNS rebinding under a wildcard bind, because a rebound host (e.g. `attacker.com`) carries consistent `Origin`/`Host`/`Sec-Fetch-Site` headers. Two coordinated changes close this: the proxy-mode UI/API now binds to `127.0.0.1` regardless of `--bind` (which governs only the proxy port), and `hostAllowed` no longer has a wildcard exception, so the host must be loopback or the exact bind address. (`internal/api/server.go` and `internal/api/originguard.go`, commit `871936f`) ```diff +// In proxy mode the UI/API binds to loopback only: --bind governs the proxy +// port, and remote collaboration is listener/teamserver mode (bearer-token auth). +uiBind := s.cfg.BindAddr +if !s.listenerMode { + uiBind…

Related

Alias · CVE (1)

Weakness · CWE (4)

  • CWE-306 Missing Authentication for Critical Functionality weakness
  • CWE-352 Cross-Site Request Forgery weakness
  • CWE-434 Unrestricted Upload of File with Dangerous Type weakness
  • CWE-942 Permissive Cross-domain Security Policy with Untrusted Domains weakness

References