Security

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)

PUBLISHED SEVERITY HIGHSTATUS PatchedGHSA-vx2m-gpq4-8j5q
Patched

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
As filed on the advisory.
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

  1. 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.
  2. Restart any running loopx serve-status process after the upgrade, including the one the macOS dashboard LaunchAgent keeps running after every login, so the new code is loaded.
  3. Until you upgrade, stop loopx serve-status when you are not using the dashboard.
  4. 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 .md or .markdown file 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 no Origin check and the preflight reply allowed Content-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.

Fix

  • #3137 (merged as 838720f) adds cors_response_headers(). It echoes the request Origin back, with Vary: Origin, only when that origin is loopback. Foreign origins and clients that send no Origin get no CORS headers, so browsers block cross-site reads. _send_json and do_OPTIONS both use it, and the write routes keep their existing Origin checks. Released in 0.4.5.
  • The dashboard keeps working, because it is served from a loopback origin.
  • The fix is still in place at main today: 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

  1. Reported privately via GitHub PVR (GHSA-vx2m-gpq4-8j5q), with a suggested fix.
  2. Maintainer merges #3137, releases 0.4.5 and publishes the advisory.
  3. Maintainer requests a CVE from GitHub.
  4. First LoopX release on PyPI (0.4.8), with the fix.
  5. CVE still not assigned; follow-up sent to GitHub. Public write-up.

Credit

Finder: Aaron Elijah Mars of Aeon. Tool: Aeon.

References