LoopX: any website can read serve-status control-plane state
LoopX serve-status sends Access-Control-Allow-Origin: * on unauthenticated GET routes, letting any website read control-plane state and private repo Markdown (GHSA-vx2m-gpq4-8j5q)
Fixed in LoopX 0.4.5 (2026-08-12) by maintainer PR #3137, which only sends CORS headers to loopback browser origins. Track the fix at loopx-project/loopx#3137.
- Product
- LoopX (PyPI loopx) in loopx-project/loopx
- Affected versions
- loopx before 0.4.5 (repository formerly huangruiteng/loopx). Fixed in 0.4.5 (2026-08-12). Every loopx release on PyPI (0.4.8 and later) includes the fix.
- Severity
- HIGH
- Status
- Patched
- Weaknesses
- CWE-346Origin Validation Error
- CWE-942Permissive Cross-domain Security Policy with Untrusted Domains
- GitHub advisory
- GHSA-vx2m-gpq4-8j5q
Published by the maintainer on 2026-08-12 - CVE
- Pending (requested from GitHub)
- Published
- Credit
- Finder: Aaron Elijah Mars of Aeon. Tool: Aeon (https://www.aeon.fun).
What users should do now
- Upgrade LoopX:
python3 -m pip install --upgrade loopx. Any release from 0.4.5 on has the fix, and every PyPI release (0.4.8 and later) includes it. - Restart any running
loopx serve-statusprocess after the upgrade, including the one the macOS dashboard LaunchAgent keeps running after every login, so the new code is loaded. - Until you upgrade, stop
loopx serve-statuswhen you are not using the dashboard. - In Chrome 142 or later, keep the Local Network Access prompt on and deny it for sites you do not expect to talk to local services.
Summary
loopx serve-status runs a local HTTP server that feeds the LoopX dashboard. It has no authentication and listens on fixed default ports: 8765 for a project and 8766 for the global view that the macOS dashboard LaunchAgent starts. Before 0.4.5 every response carried Access-Control-Allow-Origin: *. That told the browser any website may read the replies, so a page the user visited could read LoopX state and the Markdown files inside registered goal repos with a plain request and no user interaction.
Affected versions
LoopX (loopx) before 0.4.5, when loopx serve-status is running. The repository was later renamed from huangruiteng/loopx to loopx-project/loopx. Fixed in 0.4.5 (2026-08-12). LoopX is published on PyPI from 0.4.8 on, and all of those releases include the fix, so only installs from the GitHub source or releases before 0.4.5 are affected.
Impact
While serve-status ran, a web page on any origin could read:
GET /status.json: the full status view, with goal ids, run history and local absolute paths (the registry and runtime root). With--global-registry, which is how the macOS dashboard LaunchAgent runs it, this covers every project registered on the machine.GET /review-material: the content of any.mdor.markdownfile under a goal's repo root, state directory or runtime directory. In practice that means private design notes, handoff docs and evidence write-ups.GET /: endpoint discovery, including which write APIs are turned on.POST /control-plane/configure-goal/dry-run: this route had noOrigincheck and the preflight reply allowedContent-Type, so a page could read the before and after control-plane settings for a goal. It does not write anything.
LoopX's own docs/public-private-boundary.md lists local absolute paths, task ids, raw logs and active goal state as material that must stay private. The fixed, well-known ports mean a page needs no discovery step.
Not affected. The write routes (POST /reward/append and POST /control-plane/configure-goal/apply) already checked Origin and refused foreign sites, so there was no cross-site write. /review-material blocked path traversal, paths outside the goal roots and non-Markdown files. This is a read of status data and Markdown under registered goals, not arbitrary file read and not code execution.
Browser mitigation. Chrome 142 and later ask the user for Local Network Access permission before a public web page can reach a local address. If the user denies it, the read is blocked in Chrome. Other browsers were not verified.
Affected code
Permalinks at 2e6c8e6, the commit just before the fix.
- Wildcard CORS in
_send_json, the single response path for every route: loopx/status_server.py#L143-L153 - Same wildcard in
do_OPTIONS, which also allowsContent-Type: loopx/status_server.py#L155-L160 - Read routes (
/healthz,/review-material,/,/status.json): loopx/status_server.py#L766-L820 - Markdown read handler: loopx/status_server.py#L596-L636
- Control-plane dry-run with no
Origincheck: loopx/status_server.py#L520-L535 - Existing
is_loopback_origin()check, used only on write routes: loopx/status_server.py#L96-L103 - Default port 8765: loopx/status_server.py#L26
- macOS LaunchAgent defaults to port 8766 and runs
serve-status --global-registry: scripts/macos-dashboard-launchagent.sh#L10 and #L115
Fix
- #3137 (merged as 838720f) adds
cors_response_headers(). It echoes the requestOriginback, withVary: Origin, only when that origin is loopback. Foreign origins and clients that send noOriginget no CORS headers, so browsers block cross-site reads._send_jsonanddo_OPTIONSboth use it, and the write routes keep their existingOriginchecks. Released in 0.4.5. - The dashboard keeps working, because it is served from a loopback origin.
- The fix is still in place at
maintoday: loopx/status_server.py#L178-L195.
Detection (for defenders)
Look for requests to the status server (port 8765 or 8766 by default) that carry an Origin of a site the user did not open on purpose, especially GET /status.json and GET /review-material. The server only logs requests when verbose mode is on, so a proxy or endpoint tool may be the only record. A working exploit is withheld.
Timeline
Credit
Finder: Aaron Elijah Mars of Aeon. Tool: Aeon.
References
- Fix PR (merged 2026-08-12): loopx-project/loopx#3137
- Fixed release: LoopX 0.4.5
- GitHub advisory: GHSA-vx2m-gpq4-8j5q
- Related advisory published by the maintainer for the same fix: GHSA-p7c9-q3rc-f4f5
- CVE: pending (requested from GitHub)
- Affected repository: loopx-project/loopx