GitHub Advisoryadvisory · PyPI
GHSA-vx2m-gpq4-8j5q LoopX serve-status permissive CORS policy exposes control-plane state and private Markdown files
HIGH
Official page ↗
- Aliases
- CVE-2026-108372
- Ecosystem
- PyPI
- Packages
- loopx
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-346, CWE-942
- Reviewed
- 2026-10-19
Affected packages
• PyPI loopx: >= 0.0.1, < 0.4.5 → fixed in 0.4.5
Description
Summary
`loopx serve-status` sends `Access-Control-Allow-Origin: *` on every response,
including its unauthenticated `GET` routes. Because the endpoint is unauthenticated and
bound to a fixed, well-known loopback port, any website the operator visits can read
LoopX control-plane state and the contents of Markdown files inside every registered goal
repo, using a plain `fetch()` with no user interaction.
The write endpoints are *not* affected — they are correctly Origin-checked. This report is
strictly about the read paths.
Impact
While `loopx serve-status` is running, a page on any origin can run:
```js
// no interaction, no preflight, no credentials needed
const s = await (await fetch('http://127.0.0.1:8766/status.json')).json();
for (const g of s.goals ?? []) {
const m = await (await fetch(
`http://127.0.0.1:8766/review-material?goal_id=${g.id}&path=design-notes.md`
)).json();
exfiltrate(m.content);
}
```
and read back:
• `GET /status.json` — the whole status projection: goal ids, run history, and local
absolute paths (`registry`, `runtime_root`). With `--global-registry` this covers
every project registered on the machine, not just one.
• `GET /review-material?goal_id=…&path=…` — the *content* of any `.md`/`.markdown`
file under a goal's repo root, state dir, or runtime dir. In practice that is the
operator's private design notes, handoff docs, and evidence write-ups.
• `GET /` — endpoint discovery, including which write APIs are enabled.
• `POST /control-plane/configure-goal/dry-run` — this route has no Origin check at
all, and `do_OPTIONS` returns `Access-Control-Allow-Headers: Content-Type`, so a
cross-origin JSON POST passes preflight. The response leaks the `before`/`after`
control-plane configuration for a goal. (It does not write — I verified `written=false`.)
`docs/public-private-boundary.md` classifies "local absolute paths", "task ids", "raw logs
and metrics" and "active goal state that reveals current user context" as material that
must stay private. All of it is currently readable by any origin.
Two things make this more than theoretical:
1. Fixed default ports. `8765` (project) and `8766` (global) are hardcoded defaults, so
an attacker page can simply try both — no discovery step needed.
2. It is documented as a persistent service. `docs/guides/getting-started.md` documents
`loopx serve-status`, and `scripts/macos-dashboard-launchagent.sh` installs a LaunchAgent
that keeps `serve-status --global-registry --port 8766` running after every login. For
anyone following that guide, this is an always-on, machine-wide read surface.
I have graded this High on the basis of zero-interaction exfiltration of private local
material from a persistent service. It is read-only and scope-limited, so if you read the
exposure as lower I would not argue the point — you know your users' setups better than I do.
Location
`loopx/status_server.py:136-146` — `StatusRequestHandler._send_json()`:
```python
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
...
self.send_header("Access-Control-Allow-Origin", "*") # 200 ACAO: '*'
GET / Origin: https://evil.example -> 200 ACAO: '*'
GET /status.json Origin: https://evil.example -> 200 ACAO: '*'
GET /review-material?goal_id=victim-goal&path=internal-design-notes.md
Origin: https://evil.example -> 200 ACAO: '*'
content: "# Internal notes\n\nPROD_DB_HOST=db.internal.corp\nCONFIDENTIAL: ..."
```
The file content came back verbatim to a foreign origin. Matched controls from the same
harness, with both write flags enabled (i.e. the configuration most favourable to the
current design):
```
POST /reward/append Origin: https://evil.example -> 403 (origin check)
POST /control-plane/configure-goal/apply Origin: https://evil.example -> 403 (origin check)
GET /review-material ../../../../etc/hostname -> 400 (traversal blocked)
GET /review-material .md -> 400 (root check)
GET /review-material secrets.env -> 400 (extension check)
GET /status.json Origin: https://evil.example -> 200 ACAO '*' None:
body = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
origin = self.headers.get("Origin")
if origin and is_loopback_origin(origin):
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Vary", "Origin")
# no Origin header (curl, the CLI, a same-origin fetch) -> send no ACAO at all
self.end_headers()
self.wfile.write(body)
```
and the same in `do_OPTIONS`. This keeps the dashboard working — it is served from
`http://127.0.0.1:5174` and fetches `127.0.0.1:8765/8766`, which is cross-origin between two
loopback ports, which I assume is exactly why the wildcard was added — while denying every
non-loopback origin.
Two optional hardening steps, if you want them:
• Apply the existing `is_loopback_origin()` check to `POST /control-plane/configure-goal/dry-run`
and `POST /reward/dry-run` too, so the config projection is not readable cross-origin.
• Add a `Host` header check (reject a `Host` that is not loopback) to close DNS rebinding
against clients that do not send `Origin`.
Scope of review
Reviewed 26/52 identified entrypoints across 758 files (360,618 LOC) of the `loopx` package
and dashboard; tools: semgrep 1.172.0, trufflehog 3.96.0, osv-scanner 2.4.0, plus a manual
dataflow pass. Skipped: `tests/`, `r…
Related
Alias · CVE (1)
- CVE-2026-108372 Open and provider-neutral, LoopX is a lightweight state kernel and local-first control plane for loop engineering. vulnerability
References
- https://github.com/huangruiteng/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q web
- https://github.com/loopx-project/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q web
- https://nvd.nist.gov/vuln/detail/CVE-2026-108372 advisory
- https://github.com/loopx-project/loopx/pull/3137 web
- https://github.com/loopx-project/loopx/commit/838720fba42bd22f71b5bb763ec406f7bdf4c940 web
- https://github.com/huangruiteng/loopx package
- https://github.com/loopx-project/loopx/releases/tag/v0.4.5 web