CVE-2026-108372
LoopX serve-status permissive CORS policy exposes control-plane state and private Markdown files
Open and provider-neutral, LoopX is a lightweight state kernel and local-first control plane for loop engineering. From version 0.0.1 until 0.4.5, loopx serve-status in loopx/status_server.py uses StatusRequestHandler._send_json() and do_OPTIONS() to send Access-Control-Allow-Origin: * for unauthenticated responses on predictable loopback ports. A remote attacker can cause an operator who visits attacker-controlled web content to issue cross-origin requests to GET /status.json, GET /review-material?goal_id=...&path=..., GET /, and POST /control-plane/configure-goal/dry-run, exposing control-plane state, goal and run information, local absolute paths, private Markdown files below registered goal roots, and dry-run configuration projections. The affected routes do not provide cross-origin writes or arbitrary-file access. This issue is fixed in version 0.4.5.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Passive
What an attacker can affect
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
7.1
LoopX serve-status permissive CORS policy exposes control-plane state and private Markdown files
Open and provider-neutral, LoopX is a lightweight state kernel and local-first control plane for loop engineering. From version 0.0.1 until 0.4.5, loopx serve-status in loopx/status_server.py uses StatusRequestHandler._send_json() and do_OPTIONS() to send Access-Control-Allow-Origin: * for unauthenticated responses on predictable loopback ports. A remote attacker can cause an operator who visits attacker-controlled web content to issue cross-origin requests to GET /status.json, GET /review-material?goal_id=...&path=..., GET /, and POST /control-plane/configure-goal/dry-run, exposing control-plane state, goal and run information, local absolute paths, private Markdown files below registered goal roots, and dry-run configuration projections. The affected routes do not provide cross-origin writes or arbitrary-file access. This issue is fixed in version 0.4.5.
- Record
CVE-2026-108372- CVSS 4.0
- 7.1 High
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 - Weakness
- CWE-346, CWE-942
- Updated
- 2026-11-02
EUVDEUVD-2026-103186
7.1
Open and provider-neutral, LoopX is a lightweight state kernel and local-first control plane for loop engineering. From version 0.0.1 until 0.4.5, loopx serve-status in loopx/status_server.py uses StatusRequestHandler._send_json() and do_OPTIONS() to send Access-Control-Allow-Origin: * for unauthenticated responses on predictable loopback ports. A remote attacker can cause an operator who visits attacker-controlled web content to issue cross-origin requests to GET /status.json, GET /review-material?goal_id=...&path=..., GET /, and POST /control-plane/configure-goal/dry-run, exposing control-plane state, goal and run information, local absolute paths, private Markdown files below registered goal roots, and dry-run configuration projections. The affected routes do not provide cross-origin writes or arbitrary-file access. This issue is fixed in version 0.4.5.
- Record
EUVD-2026-103186- CVSS 4.0
- 7.1 High
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 - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-vx2m-gpq4-8j5q
7.1
LoopX serve-status permissive CORS policy exposes control-plane state and private Markdown files
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:
// 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-registrythis covers every project registered on the machine, not just one.GET /review-material?goal_id=…&path=…— the content of any.md/.markdownfile 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, anddo_OPTIONSreturnsAccess-Control-Allow-Headers: Content-Type, so a cross-origin JSON POST passes preflight. The response leaks thebefore/aftercontrol-plane configuration for a goal. (It does not write — I verifiedwritten=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:
- Fixed default ports.
8765(project) and8766(global) are hardcoded defaults, so an attacker page can simply try both — no discovery step needed. - It is documented as a persistent service.
docs/guides/getting-started.mddocumentsloopx serve-status, andscripts/macos-dashboard-launchagent.shinstalls a LaunchAgent that keepsserve-status --global-registry --port 8766running 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():
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
...
self.send_header("Access-Control-Allow-Origin", "*") # <-- line 142
_send_json is the single response path for all routes, so the wildcard is applied to
/status.json, /review-material, /healthz, / and the dry-run POSTs alike.
do_OPTIONS (:148-153) repeats it.
Confirmed present at main HEAD (b0c8e315, blob c9e53d1a) at the time of writing.
What is NOT affected (I checked, so you don't have to)
I tried to refute this before reporting. These controls all hold:
- Writes are properly defended. With
--enable-reward-write-apiand--enable-control-plane-write-apiboth on,POST /reward/appendandPOST /control-plane/configure-goal/applyreturn 403 "only accepts loopback browser origins" for a foreignOrigin.is_loopback_origin()does its job. There is no …
- Record
GHSA-vx2m-gpq4-8j5q- CVSS 4.0
- 7.1 High
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 - Weakness
- CWE-346, CWE-942
- Updated
- 2026-11-02
NVD
7.1
Open and provider-neutral, LoopX is a lightweight state kernel and local-first control plane for loop engineering. From version 0.0.1 until 0.4.5, loopx serve-status in loopx/status_server.py uses StatusRequestHandler._send_json() and do_OPTIONS() to send Access-Control-Allow-Origin: * for unauthenticated responses on predictable loopback ports. A remote attacker can cause an operator who visits attacker-controlled web content to issue cross-origin requests to GET /status.json, GET /review-material?goal_id=...&path=..., GET /, and POST /control-plane/configure-goal/dry-run, exposing control-plane state, goal and run information, local absolute paths, private Markdown files below registered goal roots, and dry-run configuration projections. The affected routes do not provide cross-origin writes or arbitrary-file access. This issue is fixed in version 0.4.5.
- Record
CVE-2026-108372- CVSS 4.0
- 7.1 High
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-346, CWE-942
- Updated
- 2026-11-02
OSV
7.1
LoopX serve-status permissive CORS policy exposes control-plane state and private Markdown files
Open and provider-neutral, LoopX is a lightweight state kernel and local-first control plane for loop engineering. From version 0.0.1 until 0.4.5, loopx serve-status in loopx/status_server.py uses StatusRequestHandler._send_json() and do_OPTIONS() to send Access-Control-Allow-Origin: * for unauthenticated responses on predictable loopback ports. A remote attacker can cause an operator who visits attacker-controlled web content to issue cross-origin requests to GET /status.json, GET /review-material?goal_id=...&path=..., GET /, and POST /control-plane/configure-goal/dry-run, exposing control-plane state, goal and run information, local absolute paths, private Markdown files below registered goal roots, and dry-run configuration projections. The affected routes do not provide cross-origin writes or arbitrary-file access. This issue is fixed in version 0.4.5.
- Record
CVE-2026-108372- CVSS 4.0
- 7.1 High
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 - Weakness
- CWE-346, CWE-942
- Updated
- 2026-11-02
OSVGHSA-vx2m-gpq4-8j5q
7.1
LoopX serve-status permissive CORS policy exposes control-plane state and private Markdown files
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:
// 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-registrythis covers every project registered on the machine, not just one.GET /review-material?goal_id=…&path=…— the content of any.md/.markdownfile 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, anddo_OPTIONSreturnsAccess-Control-Allow-Headers: Content-Type, so a cross-origin JSON POST passes preflight. The response leaks thebefore/aftercontrol-plane configuration for a goal. (It does not write — I verifiedwritten=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:
- Fixed default ports.
8765(project) and8766(global) are hardcoded defaults, so an attacker page can simply try both — no discovery step needed. - It is documented as a persistent service.
docs/guides/getting-started.mddocumentsloopx serve-status, andscripts/macos-dashboard-launchagent.shinstalls a LaunchAgent that keepsserve-status --global-registry --port 8766running 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():
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
...
self.send_header("Access-Control-Allow-Origin", "*") # <-- line 142
_send_json is the single response path for all routes, so the wildcard is applied to
/status.json, /review-material, /healthz, / and the dry-run POSTs alike.
do_OPTIONS (:148-153) repeats it.
Confirmed present at main HEAD (b0c8e315, blob c9e53d1a) at the time of writing.
What is NOT affected (I checked, so you don't have to)
I tried to refute this before reporting. These controls all hold:
- Writes are properly defended. With
--enable-reward-write-apiand--enable-control-plane-write-apiboth on,POST /reward/appendandPOST /control-plane/configure-goal/applyreturn 403 "only accepts loopback browser origins" for a foreignOrigin.is_loopback_origin()does its job. There is no …
- Record
GHSA-vx2m-gpq4-8j5q- CVSS 4.0
- 7.1 High
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 - Weakness
- CWE-346, CWE-942
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
loopxPyPI |
0.0.1 up to 0.4.5 | 0.4.5 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-108372 is a high vulnerability (CVSS 7.1) in Huangruiteng Loopx, published October 16, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.30%, the 23rd percentile. Fixed in loopx 0.4.5.
References
github.com/loopx-project/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q
github.com/loopx-project/loopx/pull/3137
github.com/loopx-project/loopx/commit/838720fba42bd22f71b5bb763ec406f7bdf4c940
github.com/loopx-project/loopx/releases/tag/v0.4.5
github.com/huangruiteng/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q
nvd.nist.gov/vuln/detail/CVE-2026-108372
github.com/advisories/GHSA-vx2m-gpq4-8j5q
github.com/CVEProject/cvelistV5/tree/main/cves/2026/108xxx/CVE-2026-108372.json
github.com/huangruiteng/loopx
Left out here: the rest of 2 source descriptions. The JSON record has them.