From 249680b14c98f4064f38a9044077f17baa4a586e Mon Sep 17 00:00:00 2001 From: elfrost <5491654+elfrost@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:11:21 -0400 Subject: [PATCH] docs: ArcReel GHSA-5r36-2f3p-5q87 published, fixed in v0.31.0 (25th fix) The maintainers published the advisory on 2026-09-23, fifty days after the private report and 48 after acceptance, with the fix in v0.31.0 (#2601). The embargo has lifted, so the post now carries the full detail. The shipped fix differs from the one proposed. Instead of dropping the CORS wildcard, it narrows the anonymous files 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 only a cross-origin one, and covers a stored-HTML/SVG impact the report had not named. Re-verified by differential on the verbatim decision logic (safe_join + is_public_media_path): 14 non-media files served before, 0 after, 6/6 media still served, and no parse gap between the check and the consumer. The symlink case was not run locally (no symlink privilege, no Linux VM); the maintainers' own regression test covers it. The post also carries a self-correction: the "you can find out" amplifier was overstated. The two 404 bodies do differ, but project identifiers carry 32 random bits (secrets.token_hex(4), present at the scanned commit too), so the oracle confirms a guess and cannot find one. The unchanged CORS default is recorded as hardening, not re-reported. - scan post: status resolved, update section with detail, correction, timeline - index: outcome private -> fixed, confirmed-fix counters 24 -> 25 - fixed.md: ArcReel row added; counters had drifted at 105 scans / 24 fixes - scan log: resolution suffix on the 2026-08-04 entry - work-with-me: counters had drifted at 108 scans; now 109 / 25 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/fixed.md | 5 +- docs/index.md | 8 +-- docs/scan-log.md | 2 +- docs/scans/arcreel-arcreel.md | 121 +++++++++++++++++++++++++++++++--- docs/work-with-me.md | 4 +- 5 files changed, 123 insertions(+), 17 deletions(-) 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 `