diff --git a/docs/fixed.md b/docs/fixed.md index fbf8278..fcb5a7b 100644 --- a/docs/fixed.md +++ b/docs/fixed.md @@ -1,12 +1,12 @@ --- layout: default title: Findings a maintainer fixed -description: "The 24 scans in the AI PatchLab series where the maintainer shipped a fix — what was reported, and what landed upstream." +description: "The 25 scans in the AI PatchLab series where the maintainer shipped a fix — what was reported, and what landed upstream." --- # Findings a maintainer fixed -Of 105 scans, **24** ended with a maintainer shipping a fix. This page is the +Of 109 scans, **25** ended with a maintainer shipping a fix. This page is the short version of the argument: a report is only worth writing if someone can act on it. The fastest turnaround in the series was about six hours from filing to a merged pull @@ -26,6 +26,7 @@ release notes. | 2026-08-12 | [jgravelle/jcodemunch-mcp](scans/jgravelle-jcodemunch-mcp.html) | 49 | 0 real | | 2026-08-08 | [theroyallab/tabbyAPI](scans/theroyallab-tabbyapi.html) | 18 | 2 real | | 2026-08-07 | [huangruiteng/loopx](scans/huangruiteng-loopx.html) | 57 | 1 real — withheld | +| 2026-08-04 | [ArcReel/ArcReel](scans/arcreel-arcreel.html) | 82 | 1 real — withheld | | 2026-08-03 | [the-momentum/open-wearables](scans/the-momentum-open-wearables.html) | 145 | 2 real | | 2026-07-29 | [Project-N-E-K-O/N.E.K.O](scans/project-n-e-k-o-n-e-k-o.html) | 783 | 1 real | | 2026-07-28 | [EvoScientist/EvoScientist](scans/evoscientist-evoscientist.html) | 39 | 1 real | diff --git a/docs/index.md b/docs/index.md index f8c4480..2398c27 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ --- layout: default title: AI PatchLab Scans -description: "109 curated security scans of open-source AI agents, MCP servers and LLM apps - 24 confirmed fixes, run local-first with Semgrep, Gitleaks, Trivy and pip-audit." +description: "109 curated security scans of open-source AI agents, MCP servers and LLM apps - 25 confirmed fixes, run local-first with Semgrep, Gitleaks, Trivy and pip-audit." --- # AI PatchLab Scans @@ -20,7 +20,7 @@ remediation and confidence rules to normalize the findings. > **Want this run privately against your own codebase?** I do independent > security review of AI agents, MCP servers, and LLM apps — -> [**work with me →**]({{ '/work-with-me' | relative_url }}). 109 scans, 24 confirmed fixes, methodology in the open. +> [**work with me →**]({{ '/work-with-me' | relative_url }}). 109 scans, 25 confirmed fixes, methodology in the open. > **OpenAI just launched [Daybreak](https://openai.com/index/daybreak-securing-the-world/) and Patch the Planet.** > Same remediation loop, opposite trade-off: their path is a cloud frontier model; @@ -35,7 +35,7 @@ remediation and confidence rules to normalize the findings. ## Two shorter ways in -- [**Findings a maintainer fixed**]({{ '/fixed' | relative_url }}) — the 24 that resolved +- [**Findings a maintainer fixed**]({{ '/fixed' | relative_url }}) — the 25 that resolved upstream. The shortest version of the argument: a report is only worth writing if someone can act on it. - [**Scans that found nothing**]({{ '/clean' | relative_url }}) — 40 of them, published as @@ -160,7 +160,7 @@ filed, which is the usual outcome of a clean scan. | 2026-08-07 | [huangruiteng/loopx](scans/huangruiteng-loopx.html) | 57 | 1 real — withheld | **fixed** | | 2026-08-06 | [nottelabs/notte](scans/nottelabs-notte.html) | 226 | 1 real — withheld | private | | 2026-08-05 | [Vexa-ai/vexa](scans/vexa-ai-vexa.html) | 297 | 1 real — withheld | private | -| 2026-08-04 | [ArcReel/ArcReel](scans/arcreel-arcreel.html) | 82 | 1 real — withheld | private | +| 2026-08-04 | [ArcReel/ArcReel](scans/arcreel-arcreel.html) | 82 | 1 real — withheld | **fixed** | | 2026-08-03 | [the-momentum/open-wearables](scans/the-momentum-open-wearables.html) | 145 | 2 real | **fixed** | | 2026-08-02 | [Observal/Observal](scans/observal-observal.html) | 1,117 | 1 real — withheld | private | | 2026-08-01 | [repowise-dev/repowise](scans/repowise-dev-repowise.html) | 86 | 2 real — withheld | private | diff --git a/docs/scan-log.md b/docs/scan-log.md index df378bf..4e7ff50 100644 --- a/docs/scan-log.md +++ b/docs/scan-log.md @@ -52,7 +52,7 @@ Every scan in the series, newest first, with the summary written on the day of t - **2026-08-07** — [huangruiteng/loopx](scans/huangruiteng-loopx.html) — 57 findings (57 above the medium floor), **1 real — withheld** — a **local control plane for long-running AI agent work** (3.2k★, MIT, two months old, v0.4.2 shipped the day before the scan): it holds the durable state *around* the loop — objective, gates, todos, scope, evidence, quota, handoffs — while Codex, Claude Code, Cursor or a plain shell agent executes bounded slices, and when the state says a human decision is needed it asks and waits rather than spending another turn. **57 findings across 1,601 Python files is the lowest density this series has recorded, and zero of the 57 survived curation** — second consecutive scan where the tools contributed nothing to the finding that mattered. The class is **a security check wired to some handlers of one small surface and not others, where the ones missing it are the ones that return the private material**: the mutating handlers carry a correct, working check on who is asking; the reading handlers do not; and a single transport-level default applied uniformly to every response widens "local" from *this machine* to *anything running inside this machine's browser*. Neither decision is unreasonable alone — the permissive default exists because the bundled UI genuinely runs on a different local origin, and the reads were left unguarded because the project's own contract calls the default posture *read-mostly*. **Three oracles, all the project's own words, make it a defect rather than a trade-off:** a boundary document that *enumerates* the private categories — every one of which is reachable through the unguarded reads (the [advertised-boundary test](scans/agentera-agently.html) in a new form: Agently *named* a boundary it didn't enforce, LoopX enumerated the contents of one and left a door into the room); a committed design contract that **states the correct restriction and was never implemented** — the [Vexa move](scans/vexa-ai-vexa.html) inverted, since Vexa's contract *was* enforced and its artifacts contradicted it while here nothing enforces it at all; and **the fix already present in the same file**, defined once and applied twice, forty lines from where it is missing — the [intra-repo differential](scans/project-n-e-k-o-n-e-k-o.html) at its tightest range yet, not another module or provider but *the same file*. **The differential decided the report:** same instance, same hostile origin, same second — the mutating request returned **403** naming the exact protection, the reading request returned **200** with content and an absolute path. It also kept the report honest in the other direction: the write path looked like the story, resisted every attempt, and *reporting the reads because the writes held* is the better report. A fifth seam shape — **a guard applied to a subset of one interface's implementations**, where the subset boundary (mutating vs reading) looked like the security-relevant axis and wasn't. **The supply chain is empty:** `dependencies = []` and an import sweep of all 1,601 files finds nothing outside the standard library — 15.8 MB of Python, **zero third-party runtime dependencies**, which is the real reason this scan is quiet. That produced the tooling lesson: **`{"dependencies": [], "fixes": []}` is ambiguous and I nearly published the wrong reading of it** — after four silent no-shows and one bare `[]`, a fifth degenerate pip-audit result read as a fifth failure, and it was a *true zero*; the disambiguator was the project's dependency declaration, not anything in the scan output. **9th vote**, first time the ambiguity cut toward a false positive about the *tooling* rather than a false negative about the code, and it sharpens the ask: a **per-tool coverage row**, because 0-of-0 and 0-of-47 must not render identically. Also: **17 of 39 mediums** are one GitHub-Actions hygiene rule, [fifth consecutive flood](scans/nottelabs-notte.html); 9 SHA-1 hits are all *content-addressed identifiers* (run/todo/event ids, truncated) where `usedforsecurity=False` would state intent and silence all nine, as [mistral-vibe](scans/mistralai-mistral-vibe.html) did; 2 `subprocess-injection` are a **Django** rule on a codebase with no Django, firing on an explicit argv list; all 3 gitleaks hits are fixture-tier (**9th vote**), one a doc placeholder literally valued `0123456789abcdef`. Extensive credit: every write-side clause of the component's own contract is honoured — flag defaults off, a non-local bind **refuses to start** (exception confirmed, not assumed), a preview-hash handshake rejects stale or altered payloads, unknown fields rejected not ignored; path containment on the read side is the right shape, correctly implemented, and is why this is Moderate rather than worse; and both outbound calls are host-pinned with `# noqa` comments that *explain the pin* rather than silence the linter. **Extreme velocity did not produce the defect** — 2,659 merged PRs in 60 days was the reason to look, and the one finding is not a rushed-commit seam but a design decision about where a boundary sits, made once, early, and never revisited. Strict-norm (`SECURITY.md` forbids public issues) · PVR-enabled, filed **privately, accepted first try** ([GHSA-p7c9-q3rc-f4f5](https://github.com/huangruiteng/loopx/security/advisories/GHSA-p7c9-q3rc-f4f5)), **fourth autonomous private filing**, channel state (b) · post-only, finding withheld under embargo · ✅ **RESOLVED in [v0.4.5](https://github.com/huangruiteng/loopx/releases/tag/v0.4.5) (~5 days) and the advisory PUBLISHED with me credited as reporter** — the first private filing in this series the project chose to disclose publicly rather than close quietly, so the [full detail is now on the page](scans/huangruiteng-loopx.html): `serve-status` returned `Access-Control-Allow-Origin: *` on two unauthenticated **read** endpoints whose sibling **write** endpoints already called `is_loopback_origin`, so two cross-origin requests chained — `/status.json` to enumerate absolute local paths carrying the operator's OS username, then `/review-material` to retrieve full Markdown content from the directory the project's own `docs/public-private-boundary.md` names as holding raw sub-agent prompts and traces. Fixed the way the report asked — **reuse the check already in the file** rather than adopt a new one — and shipped as **one of five** advisories in a single hardening release (the other four, none mine, closed a second `serve-status` traversal, a launcher command injection and an arbitrary write through `refresh-state --state-file`), with `tests/test_status_server_cors.py` named in the release notes as a verification suite. CWE-200 / CWE-346 / CWE-942 - **2026-08-06** — [nottelabs/notte](scans/nottelabs-notte.html) — 226 findings (206 above the medium floor), **1 real — withheld** — a **framework for building web-automation agents** (2.0k★, SSPL-1.0): give it a goal in natural language and it drives a real browser through Playwright, converting each page into a structure a model can reason over and executing the actions the model picks — a six-package monorepo plus a CLI, a workflow runtime and a hosted control plane. **Zero of the 206 scanner findings survived curation**, which is the cleanest statement yet of where this series has ended up: on a well-built codebase the scanner's job is to be *quickly dismissable*, and the finding comes from a structural question asked by hand. The class is **an asymmetric guard inside a single function** — two classes of sensitive value flow through one code path, one is bound to the context that makes releasing it safe and **fails closed** when that context doesn't match, the other is bound to nothing and the public API offers **no parameter with which a user could bind it**. The two lookups are *adjacent lines of the same `if`/`else`*, and the unbound one is the more sensitive. A fourth seam shape after [open-wearables](scans/the-momentum-open-wearables.html) (the one provider whose scheme differed), [ArcReel](scans/arcreel-arcreel.html) (an exemption crossing a default) and [Vexa](scans/vexa-ai-vexa.html) (a contract crossing its artifacts) — and the tightest: **the seam is inside one function, between two arms of one conditional**, where the closer the siblings sit the less likely the asymmetry was intended. The single check in front of the unbound path is the [tautological guard](scans/project-n-e-k-o-n-e-k-o.html) in its **third costume** — well written, does what its name says, and defeated not by evading it but by *satisfying* it, because every input it reads comes from the party it is meant to constrain. **Running the primitive decided the severity:** the finding was legible from reading, but reading could not tell "the real value is released" from "a masked stand-in is released" — the intermediate type hides itself in `repr()` — so a report that guessed would have been coherent and wrong in the one detail that matters ([Observal's lesson](scans/observal-observal.html) again). Filed **privately, accepted first try** ([GHSA-w5rf-44xh-5rq7](https://github.com/nottelabs/notte/security/advisories/GHSA-w5rf-44xh-5rq7)) — no `SECURITY.md` at any of the three locations, but PVR *deliberately enabled*, the [ArcReel rule](scans/arcreel-arcreel.html) that an opt-in outranks a missing policy file; **third autonomous private filing**, channel state (b). **The entire critical tier evaporates on reachability, by two different mechanisms:** two LiteLLM criticals describe **Proxy Server** features (OIDC cache-key collision, admin key generation, user-role modification) and notte imports LiteLLM as a *client SDK* that never starts the proxy — the exact inverse of [code-graph-rag](scans/vitali87-code-graph-rag.html), where the transport *was* live; the third is an Authlib bypass reaching the lockfile only through an **optional integrations dependency**, whose two apparent references in shipped code are an attribution comment and an unrelated string. **Version-match → reachable → actually-shipped is three gates, and every critical failed at gate two or three.** Genuine credit: the user-script runtime is a **real `RestrictedPython` sandbox that defaults to on**, with unrestricted compilation an explicit opt-in — the [advertised-boundary test](scans/agentera-agently.html) *passed*; **one lockfile for six packages**, so the monorepo is the control case with no drift to find, inverting [Kiln](scans/kiln-ai-kiln.html); a flagged `ws://` literal that is the *mirror branch* of `wss://`, preserving transport security rather than pinning plaintext; and errors carrying separate developer, user and **agent** messages so what reaches a model is chosen at the raise site. **pip-audit wrote a file after four silent no-shows — and it was `[]`**, on a lockfile Trivy mined for 135 advisories: recovery that reports nothing a second way, **9th vote** plus a new corollary that the report should surface *tool disagreement*, which union and intersection both destroy. **54 of 120 mediums** are two GHA rules, [fourth flooding vote in four scans](scans/vexa-ai-vexa.html); all **28** gitleaks hits are fixture-tier (**8th vote**) with a new wrinkle — most are real keys belonging to *other people's websites*, captured incidentally by archiving pages as offline test data. Not strict-norm, but PVR-enabled · post-only, finding withheld under embargo - **2026-08-05** — [Vexa-ai/vexa](scans/vexa-ai-vexa.html) — 297 findings (270 above the medium floor), **1 real — withheld** — an **open-source self-hosted meeting bot and transcription API** (2.6k★, Apache-2.0, **FINOS incubation**, OSPS Baseline L2 with a committed dated self-assessment): bots join Meet/Teams/Zoom, stream transcription over WebSockets into a workspace that is a git repo of Markdown the operator owns — a gateway, an identity service, an agent control plane, a runtime kernel, a Next.js terminal, an MCP server, and **three separate deployment paths**. The finding is a **composite, and no rule represented it at all** — not ranked low, *absent*: the project keeps a **machine-readable declaration of its own configuration requirements**, in which certain keys are typed as must-be-set-explicitly with a rationale tied to a dated incident (a missing value must **refuse to boot** rather than come up green and reject every call), the enforcement code is correct — and every shipped deployment artifact supplies a literal for one of them, so the check can never fire and the value it lands on is readable in the public repo. Second consecutive scan where the defect lives *between* two files that are each right, after [ArcReel](scans/arcreel-arcreel.html): on a codebase with few defects, **stop reading files and read pairs** — specifically pairs where one file states a requirement and another decides whether it is met. Two things make it reportable rather than arguable: the project's **own committed contract is the oracle** (the [docstring-oracle move](scans/rocketride-org-rocketride-server.html), but typed rather than prose — it forecloses the by-design rebuttal), and **the fix already exists in the codebase, applied elsewhere** — the enforcing idiom is used repeatedly in the same directory for less-sensitive config, so the report is *you already wrote this correctly; here is the place it is missing*, the [intra-repo differential](scans/project-n-e-k-o-n-e-k-o.html) framing. **Ship N deployment paths and the question stops being “is the default safe” and becomes “do the N defaults agree, and does anything verify that they do?”** — divergence between siblings is the signal of oversight rather than intent. **Channel state (c), and the reason to attempt rather than infer:** `SECURITY.md` forbids public issues, but PVR is *enabled* — yet the advisory API returned **HTTP 500, empty body, four consecutive attempts**, exactly like [repowise](scans/repowise-dev-repowise.html) and indistinguishable from the [working case](scans/observal-observal.html) until you try. **Much is well built:** admin routes return **404 not 403** so the surface never advertises itself; identity comes from a verified oracle with a written note that the companion cookie is display-only *because `httpOnly` stops JS reads but not a hand-crafted `Cookie` header*; `hmac.compare_digest` throughout with checks re-asserted per endpoint rather than assumed from middleware; a guard exclusion list commented to explain that the library matches by **prefix**, so a bare `/` would silently neuter the entire layer; a third-party archive pinned by version **and** verified against a committed SHA-256. Comments routinely **cite the dated incident that motivated the code** — which is precisely what made the finding findable. **pip-audit produced no output file for the fourth consecutive scan** and its meta finding is `info`, so `--min-severity medium` renders “not scanned” identically to “zero” — **8th vote**, now the top backlog item outright; Trivy carried the load alone. **92 of 189 mediums** are one mutable-action-tag rule, [third flooding vote in three scans](scans/the-momentum-open-wearables.html); inversely, **15 of 15 Dockerfiles run as root** — noise as 15 findings, a coherent recommendation as one. Strict-norm · **post-only, finding withheld** -- **2026-08-04** — [ArcReel/ArcReel](scans/arcreel-arcreel.html) — 82 findings (77 above the medium floor), **1 real — withheld** — an **open-source AI video generation workbench** (3.9k★, AGPL-3.0): feed it a novel and an agent pipeline carries it through character design, script, storyboard and finished video, fanning image/video generation across eight-plus providers with a Claude Agent SDK skill-and-subagent layout, an RPM-limited async task queue with lease-based scheduling, a FastAPI backend and a React 19 workbench. **The best-defended codebase in this series so far**, which is exactly what makes the finding interesting: the class is **two deliberate, individually-defensible decisions that compose into a capability neither intended to grant** — a documented, *build-enforced* exception to a security invariant, plus an unrelated default that ships unchanged into production. Neither half is a bug; only the pair is, and **no rule described the composite**, because a composite is not a thing a pattern matcher can see. It came from a structural question — *which routes are exempt from the guard every other route has, and what else changes who can reach them?* Filed **privately** ([GHSA-5r36-2f3p-5q87](https://github.com/ArcReel/ArcReel/security/advisories/GHSA-5r36-2f3p-5q87)) despite **no `SECURITY.md` anywhere** — because private vulnerability reporting was *deliberately enabled*, an opt-in that outranks a missing policy file, and the submission API accepted it first try (the [three-state channel model](scans/observal-observal.html) again, second autonomous private filing). The fix is **one line** and breaks nothing; the two deeper options are patterns **already implemented twice in this same codebase** for the very problem the exception solved — *you already wrote this fix; here is the third place it belongs*. The dismissals are unusually clean: three **MCP Python SDK** advisories all describe **network transports**, and ArcReel builds its agent tools with `create_sdk_mcp_server` — in-process, no listener — the exact mirror of [code-graph-rag](scans/vitali87-code-graph-rag.html), where the same CVEs *were* live because it bound StreamableHTTP on `0.0.0.0`; all **18** gitleaks hits are test fixtures and design docs (**7th vote** for the [fixture tier](scans/ag2ai-ag2.html)); 5 SQL hits are 4 Alembic DDL plus the [#1 identifier FP](scans/mnemosyne-oss-mnemosyne.html) interpolating a *constant clause* with values bound as params; and **34 of 43 mediums** are one unpinned-action rule flooding the band, one scan after [the same thing](scans/the-momentum-open-wearables.html). Root Dockerfile + `seccomp:unconfined` + `CAP_NET_ADMIN` is **not** a defect but a reasoned trade of the Docker boundary for a **bubblewrap** one nested inside it. Extensive credit earned: a containment helper whose docstring explains it uses `realpath`+prefix *because CodeQL recognises that shape as a sanitizer*; complete zip-slip coverage; a Windows-fallback command check whose docstring **enumerates its own three bypass classes** then defends each; hard startup failure (not a warning) when sandbox tooling is missing on supported platforms — the inverse of [Agently](scans/agentera-agently.html); a boot-time assertion that **refuses to start** if provider keys are in the parent environment, since the sandboxed child inherits by fork; and an auth module that excludes the empty string from its disable-values so a malformed config **cannot fail open** — the same decision point [rocketride](scans/rocketride-org-rocketride-server.html) got wrong. `pip-audit` finally produced a file after **three silent no-shows** (105 deps, a real zero) — though it disagreed with Trivy's ~dozen Python advisories, which a one-tool scan would have silently resolved either way. Not strict-norm, but PVR-enabled · post-only, finding withheld under embargo · 📝 **Accepted 2026-08-06** — the maintainers converted the submitted report into a **draft advisory**, kept the **High** severity exactly as filed, assigned **CWE-200 + CWE-862** and credited the reporter. **The first accepted private submission in this series** — and evidence the [three-state channel model](scans/observal-observal.html) needs a fourth state: *accepted* sits between "the API took the report" and "a fix shipped", and only the first of those is visible at filing time. Still embargoed — no patched version yet +- **2026-08-04** — [ArcReel/ArcReel](scans/arcreel-arcreel.html) — 82 findings (77 above the medium floor), **1 real — withheld** — an **open-source AI video generation workbench** (3.9k★, AGPL-3.0): feed it a novel and an agent pipeline carries it through character design, script, storyboard and finished video, fanning image/video generation across eight-plus providers with a Claude Agent SDK skill-and-subagent layout, an RPM-limited async task queue with lease-based scheduling, a FastAPI backend and a React 19 workbench. **The best-defended codebase in this series so far**, which is exactly what makes the finding interesting: the class is **two deliberate, individually-defensible decisions that compose into a capability neither intended to grant** — a documented, *build-enforced* exception to a security invariant, plus an unrelated default that ships unchanged into production. Neither half is a bug; only the pair is, and **no rule described the composite**, because a composite is not a thing a pattern matcher can see. It came from a structural question — *which routes are exempt from the guard every other route has, and what else changes who can reach them?* Filed **privately** ([GHSA-5r36-2f3p-5q87](https://github.com/ArcReel/ArcReel/security/advisories/GHSA-5r36-2f3p-5q87)) despite **no `SECURITY.md` anywhere** — because private vulnerability reporting was *deliberately enabled*, an opt-in that outranks a missing policy file, and the submission API accepted it first try (the [three-state channel model](scans/observal-observal.html) again, second autonomous private filing). The fix is **one line** and breaks nothing; the two deeper options are patterns **already implemented twice in this same codebase** for the very problem the exception solved — *you already wrote this fix; here is the third place it belongs*. The dismissals are unusually clean: three **MCP Python SDK** advisories all describe **network transports**, and ArcReel builds its agent tools with `create_sdk_mcp_server` — in-process, no listener — the exact mirror of [code-graph-rag](scans/vitali87-code-graph-rag.html), where the same CVEs *were* live because it bound StreamableHTTP on `0.0.0.0`; all **18** gitleaks hits are test fixtures and design docs (**7th vote** for the [fixture tier](scans/ag2ai-ag2.html)); 5 SQL hits are 4 Alembic DDL plus the [#1 identifier FP](scans/mnemosyne-oss-mnemosyne.html) interpolating a *constant clause* with values bound as params; and **34 of 43 mediums** are one unpinned-action rule flooding the band, one scan after [the same thing](scans/the-momentum-open-wearables.html). Root Dockerfile + `seccomp:unconfined` + `CAP_NET_ADMIN` is **not** a defect but a reasoned trade of the Docker boundary for a **bubblewrap** one nested inside it. Extensive credit earned: a containment helper whose docstring explains it uses `realpath`+prefix *because CodeQL recognises that shape as a sanitizer*; complete zip-slip coverage; a Windows-fallback command check whose docstring **enumerates its own three bypass classes** then defends each; hard startup failure (not a warning) when sandbox tooling is missing on supported platforms — the inverse of [Agently](scans/agentera-agently.html); a boot-time assertion that **refuses to start** if provider keys are in the parent environment, since the sandboxed child inherits by fork; and an auth module that excludes the empty string from its disable-values so a malformed config **cannot fail open** — the same decision point [rocketride](scans/rocketride-org-rocketride-server.html) got wrong. `pip-audit` finally produced a file after **three silent no-shows** (105 deps, a real zero) — though it disagreed with Trivy's ~dozen Python advisories, which a one-tool scan would have silently resolved either way. Not strict-norm, but PVR-enabled · post-only, finding withheld under embargo · 📝 **Accepted 2026-08-06** — the maintainers converted the submitted report into a **draft advisory**, kept the **High** severity exactly as filed, assigned **CWE-200 + CWE-862** and credited the reporter. **The first accepted private submission in this series** — and evidence the [three-state channel model](scans/observal-observal.html) needs a fourth state: *accepted* sits between "the API took the report" and "a fix shipped", and only the first of those is visible at filing time. Still embargoed — no patched version yet · ✅ **FIXED — published 2026-09-23**, fifty days after filing: v0.31.0 ([#2601](https://github.com/ArcReel/ArcReel/pull/2601)) shipped a different fix from the one proposed, and a stronger one. Instead of dropping the CORS wildcard, it narrowed the anonymous route to an allow-list of media directories and extensions, judged on the **resolved real path**, with a uniform 404 and `nosniff`. That closes the content class for every anonymous reader, not just a cross-origin one, and it also covers a stored-HTML/SVG impact I had not reported. **Re-verified by differential** on the verbatim decision logic: 14 non-media files served before, 0 after, 6/6 media still served, and no parse gap between check and consumer (contrast [sie](scans/superlinked-sie.html)). **One self-correction published with it:** the "you can find out" amplifier was overstated. The two 404 bodies do differ, but project identifiers carry 32 random bits, so the oracle confirms a guess and cannot find one. The unchanged CORS default is recorded as hardening, not re-reported. The first of three accepted-but-unpublished advisories to reach publication (48 days after acceptance, no nudge) — **25th fix** - **2026-08-03** — [the-momentum/open-wearables](scans/the-momentum-open-wearables.html) — 145 findings (134 above the medium floor), **2 real** — a **self-hosted platform that unifies wearable health data** (2.3k★, MIT): Garmin, Whoop, Oura, Strava, Suunto, Apple Health and Google Health behind one normalized API, plus a developer dashboard, mobile SDK, svix outgoing webhooks, Celery workers and a stdio MCP server. The first scan here where the asset at risk is **someone's heart rate and sleep data**, and a genuinely well-built codebase — which is what makes both findings interesting: each is **the sixth instance of something done right five times**. The Garmin webhook's `verify_signature` reads `garmin-client-id` and tests it for *presence*, never comparing it to the configured `settings.garmin_client_id` two files away, so **any non-empty string authenticates** — confirmed by running the shipped method (`x`, `0`, `attacker-invented` all return `True`), and demonstrated unwittingly by the project's own `TestGarminWebhookAuth` suite, which posts `"x"` and asserts 200. It is the *only* gate before dispatch, and past it sit connection revocation, OAuth-scope overwrites and health-data writes. Yet the framework around it is exemplary: `verify_signature` is an `@abstractmethod` so no provider inherits a permissive default, comparisons use `compare_digest`, and Oura and Google both **fail closed** on an unset secret — Garmin is simply the one provider whose scheme isn't HMAC, and the seam is exactly where it broke. Same shape in `docker-compose.prod.yml`, which uses `expose:` for svix and the `:?` required form for `VITE_API_URL`, then publishes **Postgres (literal password `open-wearables`) and Redis (no `--requirepass`) on `0.0.0.0`** — Redis being the Celery broker — with the trap that setting the documented `DB_PASSWORD` changes svix's DSN but *not* what the database boots with. The [tautological guard](scans/project-n-e-k-o-n-e-k-o.html) returns in a new costume: *what value would fail this check, and can the caller just send a different one?* Meanwhile **all 2 Criticals and ~33 Highs evaporate on a [lockfile split](scans/kiln-ai-kiln.html)** — they live in `mcp/uv.lock`, a **stdio** MCP client no compose file deploys, so the FastMCP/MCP-SDK HTTP-transport, WebSocket-Origin and OAuthProxy CVEs describe transports it never starts; the shipped `backend/uv.lock` yielded exactly one. SNS SHA-1 is the **third** confirmed [mandated-interop](scans/stickerdaniel-linkedin-mcp-server.html) instance (AWS `SignatureVersion 1`, with the cert URL allowlisted before fetch), and the docstring settled things **both ways** — convicting the Garmin handler, acquitting the unscoped `get_user` as an [advertised boundary](scans/agentera-agently.html) (*"Global API key"*). `pip-audit` silently produced no file for the **third scan running**, making scanner-infra meta findings' exemption from `--min-severity` the top backlog item. Not strict-norm · post + [issue #1380](https://github.com/the-momentum/open-wearables/issues/1380) · ✅ **Resolved 2026-09-01** — [PR #1507](https://github.com/the-momentum/open-wearables/pull/1507) merged and the issue closed as completed: the header is now compared against the configured value **in constant time and fails closed when unset**, with a regression test, and the compose file had already been deleted. The fix restores exactly the pattern Oura and Google already used — the sixth instance is now the sixth done right. - **2026-08-02** — [Observal/Observal](scans/observal-observal.html) — 1,117 findings (216 above the medium floor), **1 real — withheld** — a **governed registry and control plane for internal AI components** (2.3k★, Apache-2.0): submit → review → approve → version → install, with one approved component rendering into the native config dialect of **nine harnesses** (Claude Code, Cursor, Kiro, Copilot CLI + VS Code, Codex, OpenCode, Pi, Antigravity), plus a FastAPI server, Typer CLI, Next.js dashboard, Postgres + ClickHouse + Redis, and Terraform for AWS *and* Azure. Exceptionally healthy: **~100 PRs merged in 60 days from 17 distinct human contributors**, 69 issues closed, CLA bot, REUSE/SPDX headers on every file. `SECURITY.md` says *"Do not open a public GitHub issue"*, so the finding is **described by class only** — and for the first time in this series it was filed through a **fully automated private channel**: GHSA private reporting was enabled *and* `POST /security-advisories/reports` accepted it ([GHSA-2qv6-w49j-hqmq](https://github.com/Observal/Observal/security/advisories/GHSA-2qv6-w49j-hqmq)), where the identical call **500'd on [repowise](scans/repowise-dev-repowise.html) a day earlier** — so the pre-check has **three** states and the flag distinguishes none of them: *always attempt the POST*. The class: **a guard that answers the right question about the wrong noun** — a genuinely well-built validator establishes an input is safe to act on, the code acts on it, and does something *additional* the validator never had an opinion about; the lowest authenticated role reaches it, and the path runs *before* the review gate. **No rule fired on it** — the tools ranked 216 other things higher, and all six of their buckets are dismissable in a paragraph. Notably **all 7 Criticals are reference Terraform** the project ships as a deployment *example* — a **new** way a raw count misleads (mis-attribution via template, alongside vendored-code over-count on [harbor](scans/harbor-framework-harbor.html) and scanner-blind under-count on [zotero-mcp](scans/54yyyu-zotero-mcp.html)): unrestricted egress is a finding about a VPC you *operate*, not one an adopter will fork and narrow. The rest: 5 SQL hits are the [#1 identifier FP](scans/mnemosyne-oss-mnemosyne.html) (3 Alembic DDL + 2 interpolating `pg_tables` catalog output, with the reasoning **documented inline**), 2 `run-shell-injection` are `workflow_dispatch`-only ([trigger-context](scans/maziyarpanahi-openmed.html)), 11 `insecure-file-permissions` flag `chmod(0o600)` on a secrets file — the [active-harm FP](scans/stickerdaniel-linkedin-mcp-server.html) where taking the advice *widens* exposure — and 6 `unvalidated-password` are **Django** rules firing on a codebase with no Django. Extensive credit: every file write in the component-install path funnels through **one** resolver that `resolve()`s and rejects anything not `is_relative_to` the target — the right shape, applied uniformly — and user-scope hook execution is [advertised and honest](scans/agentera-agently.html), the inverse of a promised-but-unenforced boundary. Gitleaks returned a genuine **zero** across 601 Python files; coverage verified on all four tools ([0-byte lesson](scans/dataelement-clawith.html)). Also documented: **a serious finding I talked myself into and the terminal talked me out of** — `git`'s `ext::` transport looked like install-time RCE until `fatal: transport 'ext' not allowed` ended it, and `--upload-pack=` refspec injection proved inert too. Strict-norm · post-only, finding withheld under embargo - **2026-08-01** — [repowise-dev/repowise](scans/repowise-dev-repowise.html) — 86 findings, **2 real — both withheld** — a **codebase intelligence layer for AI coding agents** (4.5k★, AGPL-3.0): index a repo once and serve the dependency graph, code health, git analytics, change-risk scoring and generated docs back through **ten MCP tools**, a FastAPI backend and a Next.js dashboard (**99 PRs merged in 60 days from six distinct human authors**). Its `.github/SECURITY.md` says *"Do NOT open a public GitHub issue"*, so **no issue was filed and both findings are described by class only**. Both share one root cause worth saying out loud: **"this only runs locally" is a deployment property, not a code property** — the assumption gets established in an entrypoint script or a README quick-start, far from the code that depends on it. The lowest raw finding count in a long time, and it tracks something real: deliberate, documented decisions (one `shell=True`, one XML parser, pickle confined to local caches) instead of the same pattern scattered unexamined. Extensive credit due — the Compose path publishes to `127.0.0.1` and uses the `:?` form that rejects empty *and* unset, the API never returns provider key material, and the dependency tree is **verifiably** clean (both lockfiles parsed, one test-only advisory). Process note published in full, correcting yesterday's post: `private-vulnerability-reporting: enabled` means the *human* advisory form is live — it does **not** mean the submission **API** works. Four attempts returned HTTP 500, so private disclosure remains a manual step. diff --git a/docs/scans/arcreel-arcreel.md b/docs/scans/arcreel-arcreel.md index fa9e42b..8ef0a9a 100644 --- a/docs/scans/arcreel-arcreel.md +++ b/docs/scans/arcreel-arcreel.md @@ -10,13 +10,118 @@ date: 2026-08-04 **Repository:** [ArcReel/ArcReel](https://github.com/ArcReel/ArcReel) **Commit scanned:** `a7e78bdb` **Scan date:** 2026-08-04 -**Disclosure status:** withheld — one real finding filed privately as +**Disclosure status:** ✅ **resolved** — filed privately as [GHSA-5r36-2f3p-5q87](https://github.com/ArcReel/ArcReel/security/advisories/GHSA-5r36-2f3p-5q87), -still embargoed. **Accepted by the maintainers on 2026-08-06**: the submitted -report was converted into a draft advisory (`submission.accepted: true`, -state `triage` → `draft`), the **High** severity was kept as filed, CWE-200 and -CWE-862 were assigned, and the reporter was credited. No patched version is -published yet, so the finding stays withheld here. +accepted on 2026-08-06, fixed in +[v0.31.0](https://github.com/ArcReel/ArcReel/releases/tag/v0.31.0) and +**published by the maintainers on 2026-09-23**, fifty days after filing. The +embargo has lifted; the specifics are below. + +## Update — 2026-09-23: fixed and published + +The advisory is public, so this page no longer needs to talk around the finding. + +**[GHSA-5r36-2f3p-5q87](https://github.com/ArcReel/ArcReel/security/advisories/GHSA-5r36-2f3p-5q87)** +— *Anonymous project-file endpoint served any file inside a project directory.* +High (CVSS 3.1 7.5, `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`), CWE-200 / CWE-862, +vulnerable `< 0.31.0`, patched in **0.31.0**, credited to +[@elfrost](https://github.com/elfrost) as reporter. + +**What was withheld.** The reviewed exception was `GET +/api/v1/files/{project_name}/{path}`, which lives on a separate `public_router` +mounted with no authentication dependency (`server/app.py:609` at the scanned +commit). The decision was deliberate and tested, since the route sits in the +auth-coverage test's `PUBLIC_OPERATIONS` list, and it was justified in writing: +`` and `