diff --git a/ROADMAP.md b/ROADMAP.md index 1d04802..d122e04 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -70,7 +70,9 @@ - [x] Unaudited-lockfile coverage warning when a repo ships both a lockfile and open version floors (2026/08/21) - [x] SQL-identifier rule cluster + mutable-action-tag downgraded to low confidence (2026/08/21) - 49% of historical Semgrep output - [x] Gitleaks confidence keyed on rule id and placeholder shape (2026/08/21) - 93% of historical hits leave the `high` tier -- [ ] Rule applicability check: suppress a framework-specific rule when the framework is not a declared dependency (`sqlalchemy-execute-raw-query` fired 157 times on a project with no SQLAlchemy) +- [ ] Rule applicability check: suppress a framework-specific rule when the framework is not a declared dependency (`sqlalchemy-execute-raw-query` fired 157 times on a project with no SQLAlchemy; `django-no-csrf-token` went 45 for 45 false on FastAPI templates in fossasia/voxbento, 2026-09-28) +- [ ] Gitleaks: fingerprint hits by secret hash and report one finding with N locations (fossasia/voxbento: one CI-only key quoted across `docs/plans/` surfaced as 49 high-severity findings) +- [ ] Semgrep partial-coverage advice keyed on error type: recommend a higher `--timeout` only when rules timed out; parse errors never succeed on retry (flagged 2026-09-13, again 2026-09-28 with 0 timeouts in 40 errors) - [ ] Report lockfile and open-floor dependency sets as separate rows naming the install path, rather than one merged verdict - [ ] Timeouts on the semgrep, trivy and gitleaks runners (pip-audit done; the others share the same hang risk) - [ ] Exempt meta findings from `--ignore-file` suppression as well as `--min-severity` diff --git a/corpus/verdicts/fossasia-voxbento.json b/corpus/verdicts/fossasia-voxbento.json new file mode 100644 index 0000000..d6e91f3 --- /dev/null +++ b/corpus/verdicts/fossasia-voxbento.json @@ -0,0 +1,368 @@ +{ + "repository": "C:\\Users\\Elfrost\\AppData\\Local\\Temp\\ai-patchlab-clone-l0a0yyml\\repo", + "generated_at": "2026-09-28T13:09:40.774067+00:00", + "total_dismissed": 177, + "by_reason": { + "placeholder-secret": 50, + "mitigated-in-app": 46, + "not-reachable": 40, + "by-design": 25, + "below-min-severity": 12, + "sample-or-demo": 2, + "domain-noun-collision": 1, + "dependency-currency": 1 + }, + "records": [ + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "semgrep", + "rule": "package_managers.dependabot.dependabot-missing-cooldown.dependabot-missing-cooldown", + "count": 3, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "trivy", + "rule": "No HEALTHCHECK defined", + "count": 2, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "trivy", + "rule": "postcss-selector-parser: Postcss: Denial of Service via uncontrolled recursion in AST Serialization", + "count": 2, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "semgrep", + "rule": "package_managers.uv.uv-missing-dependency-cooldown.uv-missing-dependency-cooldown", + "count": 1, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "trivy", + "rule": "body-parser: body-parser: Denial of Service via invalid limit option", + "count": 1, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "trivy", + "rule": "joi: @hapi/joi: joi: Prototype pollution via untrusted input in schema configuration", + "count": 1, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "trivy", + "rule": "joi: object().rename() with a template target can set the validated object's prototype", + "count": 1, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "scanner", + "reason_code": "below-min-severity", + "tool": "trivy", + "rule": "starlette: Starlette: Information disclosure due to improper HTTP request path validation", + "count": 1, + "verdict": "", + "detail": "Below the --min-severity floor (medium)." + }, + { + "source": "curation", + "reason_code": "placeholder-secret", + "tool": "gitleaks", + "rule": "generic-api-key", + "count": 49, + "verdict": "false-positive", + "detail": "One value, 49 times: the CI-only API_KEY_ENCRYPTION_KEY (prefixed `ci-`) set in .github/workflows/tests.yml (x2) and copied into 17 files under docs/plans/ (x47). A test-workflow fixture, not a deployment credential." + }, + { + "source": "curation", + "reason_code": "placeholder-secret", + "tool": "gitleaks", + "rule": "generic-api-key", + "count": 1, + "verdict": "false-positive", + "detail": ".env.example:14 - JWT_SECRET is empty; the match runs onto the next line's variable name." + }, + { + "source": "curation", + "reason_code": "mitigated-in-app", + "tool": "semgrep", + "rule": "python.django.security.django-no-csrf-token.django-no-csrf-token", + "count": 45, + "verdict": "false-positive", + "detail": "Django rule on a FastAPI + Jinja app. Both auth cookies (admin_token, user_token) are set HttpOnly + SameSite=Lax, which browsers withhold on cross-site POST; the admin router's writes are POST (39 routes) or DELETE (1); the developer dashboard adds a double-submit CSRF token checked with secrets.compare_digest. Scope: the rule inspects POST forms only; GET handlers that set cookies or write rows (Lax does not cover top-level GET navigations) were not traced in this curation." + }, + { + "source": "curation", + "reason_code": "by-design", + "tool": "semgrep", + "rule": "yaml.github-actions.security.github-actions-mutable-action-tag", + "count": 17, + "verdict": "hardening", + "detail": "Same-rule flood across six workflows (tests, docs, codeql, bandit, deploy, semgrep): SHA-pinning third-party actions is supply-chain hardening, not a vulnerability." + }, + { + "source": "curation", + "reason_code": "sample-or-demo", + "tool": "semgrep", + "rule": "javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket", + "count": 2, + "verdict": "false-positive", + "detail": "website/docs/api/websocket/{booth,captions}.mdx:16 - a ws:// URL in API documentation text, not code." + }, + { + "source": "curation", + "reason_code": "by-design", + "tool": "semgrep", + "rule": "html.security.audit.missing-integrity.missing-integrity", + "count": 2, + "verdict": "hardening", + "detail": "home.html:11 + listener-event.html:4 load the Tailwind Play CDN script (cdn.tailwindcss.com), which is unversioned and cannot carry an SRI hash. Vendoring a Tailwind build removes the third-party script; the project already did so for its error pages in #263." + }, + { + "source": "curation", + "reason_code": "mitigated-in-app", + "tool": "semgrep", + "rule": "python.flask.security.xss.audit.direct-use-of-jinja2.direct-use-of-jinja2", + "count": 1, + "verdict": "false-positive", + "detail": "email_sender.py:76 renders email/onboarding.html through Starlette's Jinja2Templates, whose environment uses select_autoescape() (starlette 1.1.0 templating.py:95) - .html templates are escaped." + }, + { + "source": "curation", + "reason_code": "by-design", + "tool": "semgrep", + "rule": "python.sqlalchemy.performance.performance-improvements.len-all-count", + "count": 1, + "verdict": "not-applicable", + "detail": "webhooks.py:80 - SQLAlchemy performance lint, not a security rule." + }, + { + "source": "curation", + "reason_code": "by-design", + "tool": "semgrep", + "rule": "javascript.browser.security.insufficient-postmessage-origin-validation.insufficient-postmessage-origin-validation", + "count": 1, + "verdict": "not-applicable", + "detail": "embed-player.js:189 headless-embed listener: enforces the configured allowed_origins list when set and requires source='voxbento-parent'; it can only play, pause or set the volume of the embedded audio - the control surface an embeddable player is meant to give its host page." + }, + { + "source": "curation", + "reason_code": "domain-noun-collision", + "tool": "semgrep", + "rule": "python.lang.security.audit.logging.logger-credential-leak.python-logger-credential-disclosure", + "count": 1, + "verdict": "false-positive", + "detail": "tts/worker.py:51 logs 'Deepgram API key not found for Event ' - the event id, never the key." + }, + { + "source": "curation", + "reason_code": "by-design", + "tool": "trivy", + "rule": "Image user should not be 'root'", + "count": 2, + "verdict": "hardening", + "detail": "Dockerfile + floor-bot/Dockerfile run as root; a USER line is container hardening, not a vulnerability." + }, + { + "source": "curation", + "reason_code": "by-design", + "tool": "trivy", + "rule": "'apt-get' missing '--no-install-recommends'", + "count": 2, + "verdict": "hardening", + "detail": "Dockerfile + floor-bot/Dockerfile: image-size hygiene." + }, + { + "source": "curation", + "reason_code": "dependency-currency", + "tool": "trivy", + "rule": "CVE-2026-54283", + "count": 1, + "verdict": "hardening", + "detail": "starlette 1.1.0 -> 1.3.1 in uv.lock (transitive via fastapi 0.141.1, which requires only starlette>=0.46.0). request.form() limits are ignored for urlencoded bodies; reachable before login through the FastAPI Form() parameters on the auth routes. No open Dependabot PR at scan time (starlette is transitive). Fix: uv lock --upgrade-package starlette." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "CVE-2026-59890", + "count": 1, + "verdict": "not-applicable", + "detail": "setuptools 78.1.1: MANIFEST.in exclusion bypass when building an sdist on macOS APFS/HFS+ - build-time packaging only; the portal never builds an sdist." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "js-yaml", + "count": 7, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-53550, CVE-2026-59869, CVE-2026-84375, GHSA-5p4m-2wfm-xmqj." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "fast-uri", + "count": 6, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-13676, CVE-2026-16221, CVE-2026-18446, CVE-2026-75899, CVE-2026-75975, CVE-2026-76172." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "brace-expansion", + "count": 3, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-13149, CVE-2026-14257, CVE-2026-69152." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "svgo", + "count": 3, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-73650, CVE-2026-84369, CVE-2026-84370." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "browserslist", + "count": 2, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-73088, CVE-2026-73089." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "image-size", + "count": 2, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2025-71329, CVE-2025-71330." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "nanoid", + "count": 2, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-67213, CVE-2026-67214." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "postcss", + "count": 2, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-69153, CVE-2026-73646." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "qs", + "count": 2, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-82417, CVE-2026-82562." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "serialize-javascript", + "count": 2, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-34043, GHSA-5c6j-r48x-rmvq." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "webpack-dev-server", + "count": 2, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-14620, CVE-2026-14631." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "@swc/html", + "count": 1, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-72925." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "baseline-browser-mapping", + "count": 1, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-45819." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "colord", + "count": 1, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-85062." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "http-proxy-middleware", + "count": 1, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-55602." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "shell-quote", + "count": 1, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-13311." + }, + { + "source": "curation", + "reason_code": "not-reachable", + "tool": "trivy", + "rule": "uuid", + "count": 1, + "verdict": "not-applicable", + "detail": "website/package-lock.json - the Docusaurus 3.10.1 docs site. CI runs npm ci + npm run build and publishes website/build as static files (docs.yml -> GitHub Pages); nothing in portal/ imports it, so this package runs only at build time. Advisories: CVE-2026-41907." + } + ] +} diff --git a/docs/fixed.md b/docs/fixed.md index 97e2aa6..8e4fe9c 100644 --- a/docs/fixed.md +++ b/docs/fixed.md @@ -6,7 +6,7 @@ description: "The 26 scans in the AI PatchLab series where the maintainer shippe # Findings a maintainer fixed -Of 111 scans, **26** ended with a maintainer shipping a fix. This page is the +Of 112 scans, **26** 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 diff --git a/docs/index.md b/docs/index.md index 65febd8..1214dda 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ --- layout: default title: AI PatchLab Scans -description: "111 curated security scans of open-source AI agents, MCP servers and LLM apps - 26 confirmed fixes, run local-first with Semgrep, Gitleaks, Trivy and pip-audit." +description: "112 curated security scans of open-source AI agents, MCP servers and LLM apps - 26 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 }}). 111 scans, 26 confirmed fixes, methodology in the open. +> [**work with me →**]({{ '/work-with-me' | relative_url }}). 112 scans, 26 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; @@ -103,7 +103,7 @@ login and static assets. Fifty-two flagged, none reported. ## All scans -111 scans, newest first. **Findings** is the raw count the tools produced; +112 scans, newest first. **Findings** is the raw count the tools produced; **Real** is what survived curation. The gap between those two columns is the entire job. @@ -118,6 +118,7 @@ filed, which is the usual outcome of a clean scan. | Date | Repository | Findings | Real | Outcome | | --- | --- | ---: | --- | --- | +| 2026-09-28 | [fossasia/voxbento](scans/fossasia-voxbento.html) | 168 | 0 first-party — dependency | — | | 2026-09-24 | [superdesigndev/treg](scans/superdesigndev-treg.html) | 96 | 1 real — withheld | private | | 2026-09-23 | [can4hou6joeng4/boss-agent-cli](scans/can4hou6joeng4-boss-agent-cli.html) | 26 | 1 real — withheld | private | | 2026-09-22 | [overwirehq/claude-code-telegram](scans/overwirehq-claude-code-telegram.html) | 54 | 0 first-party — dependency | — | diff --git a/docs/scan-log.md b/docs/scan-log.md index f0e1638..5dc8a5a 100644 --- a/docs/scan-log.md +++ b/docs/scan-log.md @@ -6,8 +6,9 @@ description: "The complete AI PatchLab scan log: every public repository scanned # Full scan log -Every scan in the series, newest first, with the summary written on the day of the scan. 111 scans. For the compact index, see the [scan log home]({{ '/' | relative_url }}). +Every scan in the series, newest first, with the summary written on the day of the scan. 112 scans. For the compact index, see the [scan log home]({{ '/' | relative_url }}). +- **2026-09-28** — [fossasia/voxbento](scans/fossasia-voxbento.html) — 168 findings at `medium+` (83 high, 82 medium, 3 meta), **0 first-party defects — dependency currency, post-only** — FOSSASIA's **real-time interpretation platform for live events** (1.5k★, Apache-2.0): interpreters broadcast translated audio from a browser tab, attendees listen from theirs, and a FastAPI portal handles accounts and JWT sessions, the admin console, WebSocket booth coordination, transcription and translation workers, and a developer platform with OAuth2 clients and API keys. One of the most active targets in the series — 167 merged PRs from 23 human authors and 77 closed issues in 60 days; no `SECURITY.md`, private reporting off, not strict-norm. **Two families explain most of the 168.** Semgrep's `django-no-csrf-token` fired 45 times on a FastAPI app's Jinja templates, where the auth cookies are `HttpOnly` + `SameSite=Lax`, the admin router's writes are POST or DELETE, and the developer dashboard adds a double-submit token; Gitleaks counted **one** CI-only encryption key 49 times because 17 files under `docs/plans/` quote the test workflow. **The defaults fail closed:** the compose file's `SECRET_KEY=change-me` fallback cannot boot a production portal, because the FastAPI lifespan runs `validate_production_secrets()` and refuses known-weak values outside debug (tested, and debug defaults off), while third-party provider keys sit under a separate rotating `MultiFernet` key. The one actionable item is **Starlette 1.1.0 → 1.3.1** (CVE-2026-54283, form limits ignored for urlencoded bodies): transitive via FastAPI, reachable on the unauthenticated `/register` and `/login` routes, fixed by `uv lock --upgrade-package starlette`, with no open Dependabot PR at scan time. 39 of the 45 Trivy advisories are the Docusaurus docs site's build chain — static output the portal never imports, in the one ecosystem (`npm`) the repo's Dependabot does not cover. **Scope stated in the post:** every tool finding is accounted for, but the OAuth2 provider, the API-key platform and the WebSocket layer were not swept by hand this time. Coverage `partial` and stated so — Semgrep's 40 errors are all template and Dockerfile parse errors (zero timeouts), and pip-audit left `uv.lock` unaudited while Trivy read it. Quality gate not met · post-only, nothing filed - **2026-09-24** — [superdesigndev/treg](scans/superdesigndev-treg.html) — 96 findings at `medium+` (1 critical, 23 high, 69 medium, 3 meta), **1 real — withheld, reported privately by email** — **"OpenRouter for agent tools"** (3k★, source-available; hosted at treg.to and self-hostable), a multi-tenant server holding a team's provider credentials Fernet-encrypted and proxying calls to 3,000+ upstream endpoints with the credential injected server-side so it never reaches the caller. Its `SECURITY.md` makes two load-bearing promises: the proxy never hands a key to the caller, and outbound requests are *"re-resolved at call time and internal/metadata addresses are refused."* **Zero of the 96 scanner findings is the vulnerability** — the real item came from tabulating the server's outbound request paths and asking which the call-time host guard actually covers. It does have a proper guard, `host_is_public` in `infra/upstream/ssrf.py`, and its address classification is genuinely thorough (every loopback/link-local/CGNAT/NAT64 and decimal/hex/octal/short-form IPv4 literal I tried was refused). The finding is that the guard is wired into the main proxy path and **not** into a sibling outbound path a plain member can drive; the registration-time check that does run there is, by its own docstring, a static check that allows any DNS name, so a name resolving to an internal address passes it and nothing re-resolves before the request goes out — a member-authenticated, semi-blind [SSRF on the unguarded sibling of a guarded transport](scans/liaohch3-claude-tap.html). Confirmed with the [run-the-primitive](scans/observal-observal.html) discipline: treg's own two guard functions run side by side on a host that resolves internally return "allowed" (registration) and "refused" (call-time), and the sibling path calls only the first — one differential, using the project's code not my reasoning. Graded **Medium**: the injected credential on the affected path is the member's own, so it is internal + cloud-metadata reach, not cross-tenant theft; the org boundary itself held everywhere ([the probe could return "no" on isolation and did](scans/tracecathq-tracecat.html)). **The fix is one guard applied at the outbound path** and changes nothing for a legitimate public target. The 96: seventeen `sqlalchemy-text` are the [#1 identifier FP](scans/aurelio-labs-semantic-router.html) (Alembic `SET`/`CREATE INDEX CONCURRENTLY` from module constants + a test-reset `TRUNCATE` over dialect-quoted table names, no value interpolated); eighteen `v-html` render the project's own static tutorial/snippet HTML with an `esc(&<>)` helper on every interpolated field; four `insecure-file-permissions` are the project [tightening to `0o700`](scans/realiti4-claude-swap.html) (the rule's suggestion would loosen them); one Trivy critical is `anyio` 4.14.1→4.14.2 IDNA hostname-confusion, a worthwhile lockfile bump reachable only by a network-positioned attacker against a non-ASCII upstream host. Coverage `partial` and stated so — Semgrep hit a Windows per-file temp error on five large modules (recovered by a targeted re-run of just those five), and pip-audit left `uv.lock` unaudited while Trivy read it; neither touches the finding. **Strict-norm** (`SECURITY.md` forbids public vuln issues, PVR disabled — checked `{"enabled":false}` — names an email) · reported **privately by email to the maintainer**; post-only here, finding withheld at class level · sent by email 2026-09-24, ten minutes after this entry was published - **2026-09-23** — [can4hou6joeng4/boss-agent-cli](scans/can4hou6joeng4-boss-agent-cli.html) — 26 findings at `medium+` (7 high, 16 medium, 3 meta), **1 real — withheld, filed privately** — a **CLI for the BOSS Zhipin recruitment platform** (2k★, MIT; a human drives it from a terminal wizard, an agent drives the same workflows over a JSON-envelope API, an ~77-tool MCP server, or a Python SDK) that authenticates by reusing the operator's own logged-in browser session. Responsive maintainer — 67 merged PRs from 7 human authors in 60 days — which is why it cleared the pre-check. **Zero of the 26 scanner findings is the vulnerability**, which is the whole point of the entry: the real item is the *absence* of a check on an otherwise-working local channel, the shape no rule can match, and it is the [desktop-loopback-inversion](scans/liaohch3-claude-tap.html) class — a `127.0.0.1` control surface that binds neither *who* may call it nor which host the request claims, driving privileged browser actions against a site where the operator is authenticated. Confirmed with a **server-side exploit primitive against the shipped daemon** (a foreign `Host` is served, a cross-origin "simple" request reaches the command channel with no preflight, a hostile `Origin` is not rejected) plus a read of the extension source for the browser half — the [run-the-primitive discipline](scans/observal-observal.html), because reading alone could not separate "the channel is drivable" from "the channel is drivable *and* blind does not blunt it." Graded **High not Critical** on an honest precondition (the bridge component must be running, and the user has to start it deliberately; once started it stays up while the browser side is connected — *corrected 2026-09-24: first published as "a bounded window", the one claim reasoned about rather than checked, and wrong both ways; the private report was corrected the same day*) and the sensitive scope is the one site the tool is built around; **the fix was written before it was named**, narrow and verified not to break the tool's own two legitimate clients ([write the patch first](scans/roflcoopter-viseron.html)). The 26: eight SQL hits (five high) are the [#1 identifier FP](scans/aurelio-labs-semantic-router.html) — every interpolation is a hardcoded table/column name with values bound as `?`, settled by reading the call sites; eight of sixteen mediums are one `github-actions-mutable-action-tag` flood; the rest are two `sha1()[:16]` dedup-id digests, two `urllib` calls to a local DevTools endpoint and a build script, a React rule on a loopback health-check `fetch`, and a Python-3.7 compatibility rule that is not a security check. Coverage `partial` and stated so — Semgrep did not cover every file and the dependency scan left `uv.lock` unaudited (Trivy read it, the [luck that hides the bug](scans/roflcoopter-viseron.html)), neither touching the finding. **Strict-norm** (`SECURITY.md` forbids public vulnerability issues, gives an email and a PVR link) · PVR-enabled, filed **privately, accepted into triage first try** ([GHSA-xp54-2hxc-w78q](https://github.com/can4hou6joeng4/boss-agent-cli/security/advisories/GHSA-xp54-2hxc-w78q)) **with the `vulnerabilities` array** — the [viseron rule](scans/roflcoopter-viseron.html) holds again · post-only, finding withheld at class level under embargo - **2026-09-22** — [overwirehq/claude-code-telegram](scans/overwirehq-claude-code-telegram.html) — 54 findings at `medium+` (1 critical, 21 high, 30 medium), **0 first-party defects — dependency currency, post-only** — a **Telegram bot that gives authorised users remote access to Claude Code**, i.e. it runs file and Bash tools on a host machine by design, so the whole security surface *is* the boundary around that execution (2k★, MIT, org-backed; active — 15 merged PRs from 7 authors and 9 closed issues in 60 days; strict-norm: real `SECURITY.md`, PVR enabled). Picked with the manual queue at zero. **The story is what precise security documentation looks like.** The real boundary is the `can_use_tool` callback that stops Claude — when steered off-course by content it reads mid-task — from acting outside the approved directory, and the maintainers know and *state* exactly what it covers: path validation is scoped in the docs to precisely six tools (`Read`, `Write`, `Edit`, `MultiEdit`, `NotebookEdit`, `NotebookRead`), while `Grep`/`Glob`/`LS` are deliberately left to the OS sandbox. Reading that as "Read is guarded but Grep is not, so the boundary is incomplete" is the plausible-but-wrong finding this site exists to resist — the boundary is [advertised, not accidental](scans/tracecathq-tracecat.html), and `ROADMAP-v2.md` even records the SDK's own `CanUseToolShadowedWarning` naming every tool the callback will never see, filed as tracking item #221. Two more choices earn credit: guarded tools are deliberately *stripped from* the SDK `allowed_tools` list (a pre-approved tool never produces a `can_use_tool` request, so leaving them in would render the checks silently inert — [issue #219](https://github.com/overwirehq/claude-code-telegram/issues/219)), and `autoAllowBashIfSandboxed` is disabled whenever the boundary checks must run, closing a second bypass. That is a maintainer who traced how the framework resolves permissions rather than trusting that a config allow-list is an enforcement boundary — it isn't, and the code says so. **When the machine is built and documented this carefully, the finding moves to the dependency manifest.** The 30 Trivy advisories in `poetry.lock` split cleanly by reachability: **17 are opt-in-only and not reachable on a default install** — `starlette` (6) and `python-multipart` (3) need the FastAPI webhook server (`ENABLE_API_SERVER=false` default); `pyjwt` (5) needs token auth (`ENABLE_TOKEN_AUTH=false` default, and the SECURITY.md documents token auth as non-functional, [#58](https://github.com/overwirehq/claude-code-telegram/issues/58)); the MCP SDK highs (3) need MCP enabled — while **13 are unconditionally installed** (`anyio` incl. the critical IDNA/TLS advisory, the `cryptography`/OpenSSL cluster, `urllib3`, `idna`, `requests`, `pydantic-settings`, `python-dotenv`) and are the genuine currency gap. The direct deps are current; the drift is transitive, which the repo's existing `.github/dependabot.yml` (configured for *version* updates, not *security* updates) will not raise on its own. **No advisory filed** — there is no first-party vulnerability. Of the other 24: 16 `github-actions-mutable-action-tag` (SHA-pin hardening); one `pull_request_target` checkout wrapped in a 40-line threat-model header (read-only tool allowlist, secrets scrubbed, "worst case is a prompt-injected review comment" — [the trigger decides severity](scans/lightseekorg-tokenspeed.html)); two `sqlalchemy-execute-raw-query` [identifier FPs](scans/aurelio-labs-semantic-router.html) (only a generated run of `?` placeholders is interpolated, values bound via `execute(query, params)`); and three gitleaks hits that are a `your-api-secret` placeholder plus two fake tokens in a test asserting the project's own `_redact_secrets()` helper scrubs them ([credited defence](scans/realiti4-claude-swap.html)). Coverage `partial` and honestly so: pip-audit resolved **no dependencies** from a `pyproject.toml` declaring `dynamic = ["dependencies"]` — reading identically to "clean" — and Trivy's `poetry.lock` read carried the run, the [same dependency blind spot](scans/harnessrouter-harnessrouter.html) as yesterday reached by a different manifest shape. diff --git a/docs/scans/fossasia-voxbento.md b/docs/scans/fossasia-voxbento.md new file mode 100644 index 0000000..17695ad --- /dev/null +++ b/docs/scans/fossasia-voxbento.md @@ -0,0 +1,204 @@ +--- +layout: default +title: "fossasia/voxbento: security scan" +description: "Security scan of fossasia/voxbento: 168 findings at medium+, 0 first-party defects after curation; one reachable dependency advisory (Starlette). Local-first curated review: Semgrep, Gitleaks, Trivy, pip-audit." +date: 2026-09-28 +--- + +# fossasia/voxbento — security scan + +**Repository:** [fossasia/voxbento](https://github.com/fossasia/voxbento) +**Commit scanned:** `81a6bbf` (`dev`, the default branch) +**Scan date:** 2026-09-28 +**Disclosure status:** post-only — nothing first-party survived curation, so nothing was filed upstream. The one actionable item is a published advisory in a transitive dependency, fixed by one lockfile command. + +## Summary + +| Severity | Count | +| --- | ---: | +| Critical | 0 | +| High | 83 | +| Medium | 82 | +| Low | 0 | +| Info | 3 | + +**Total findings:** 168 at `--min-severity medium` (0 first-party defects after curation; 1 reachable dependency advisory; the 3 info rows are coverage meta findings) + +Voxbento is FOSSASIA's real-time interpretation platform for live events: interpreters +broadcast translated audio from a browser tab, attendees listen from theirs, and a +FastAPI portal handles everything around the audio — accounts and JWT sessions, the +admin console, booth coordination over WebSocket, background transcription and +translation, and a developer platform with OAuth2 clients and API keys (1.5k★, +Apache-2.0). It is one of the most active projects this series has picked: 167 merged +pull requests from 23 human authors and 77 closed issues in the 60 days before the scan. + +**Scope of this write-up.** Every finding family below was checked against the code at +its call site. This page is *not* a manual review of the surfaces no rule inspects — the +OAuth2 provider, the developer API-key platform and the booth WebSocket layer were not +swept by hand this time. Read the result as "the tools' 168 findings are accounted for", +not as a clean bill of health for those surfaces. + +## Scan coverage + +| Tool | Status | Detail | +| --- | --- | --- | +| `semgrep` | `partial` | Semgrep did not cover every file it was pointed at | +| `gitleaks` | `ran` | Ran without reporting a coverage problem. | +| `trivy` | `ran` | Ran without reporting a coverage problem. | +| `dependency-scan` | `partial` | A shipped lockfile was not covered by the dependency scan | +| `ai-security-review` | `not_run` | AI security review is disabled | + +**Coverage complete:** no. Semgrep's 40 errors are all parse errors — 38 `PartialParsing` +and 2 syntax errors on the Jinja HTML templates and the two Dockerfiles, with **zero rule +timeouts** — so no first-party Python module went unexamined. `dependency-scan` +(pip-audit) resolved the version floors in `pyproject.toml` and did not read the shipped +`uv.lock`; Trivy read `uv.lock` directly, and every dependency finding below comes from it. + +## Top findings + +There is no first-party finding to lead with. The one item worth acting on is a +dependency advisory that happens to sit on the login path. + +### 1. Starlette 1.1.0 in `uv.lock` — form limits ignored for urlencoded bodies (CVE-2026-54283) + +- **File:** `uv.lock` (starlette 1.1.0, pulled in transitively by fastapi 0.141.1) +- **Tool:** trivy +- **Confidence:** high that it applies; the call sites are unauthenticated +- **Why it matters:** Starlette's `request.form()` enforces `max_fields` and + `max_part_size` for multipart bodies but silently ignores them for + `application/x-www-form-urlencoded`, so a URL-encoded body of any size is parsed in + full. Voxbento calls `request.form()` directly on its `/register` and `/login` routes + (`portal/routers/auth.py:114`, `:207`), which answer before any authentication. It is a + resource-exhaustion advisory, not a data-exposure one. +- **Recommendation:** `uv lock --upgrade-package starlette`. FastAPI 0.141.1 requires only + `starlette>=0.46.0`, so 1.3.1 resolves without touching FastAPI. The repository's daily + `uv` Dependabot had no open pull request for it at scan time — Starlette is a transitive + dependency, and version updates target the direct ones. + +### 2. The docs-site build chain — 39 advisories that never run in production + +- **File:** `website/package-lock.json` +- **Tool:** trivy +- **Confidence:** high that the versions are behind; not reachable in a deployed voxbento +- **Why it matters:** 39 of the 45 Trivy findings — `js-yaml` (7), `fast-uri` (6), + `brace-expansion` (3), `svgo` (3) and 13 more packages — belong to `website/`, the + Docusaurus 3.10.1 documentation site. CI runs `npm ci` and `npm run build` and publishes + `website/build` as static files to GitHub Pages; nothing under `portal/` imports it. +- **Recommendation:** refresh the docs lockfile when convenient. `.github/dependabot.yml` + covers `uv`, `github-actions` and `docker`, but not `npm`, so this lockfile has no + automated updates today; adding an `npm` entry for `/website` would keep it current. + +### 3. Hardening, not vulnerabilities + +- **17 × `github-actions-mutable-action-tag`** across six workflows — SHA-pinning + third-party actions is supply-chain hardening. +- **2 × `missing-integrity`** (`home.html:11`, `listener-event.html:4`) — both load the + Tailwind Play CDN script, which is unversioned and cannot carry an SRI hash. Vendoring a + Tailwind build removes the third-party script from the listener page; the project already + did exactly that for its error pages in + [#263](https://github.com/fossasia/voxbento/pull/263). +- **2 × container runs as root, 2 × `apt-get` without `--no-install-recommends`** (portal + and floor-bot images) — image hygiene. + +## Why the rest were dismissed + +Of the 168 findings, 165 are accounted for below; the other 3 are the coverage meta findings +above. The scanner itself dropped 12 more under the `--min-severity` floor. + +| Reason | Findings | +| --- | ---: | +| `placeholder-secret` | 50 | +| `mitigated-in-app` | 46 | +| `not-reachable` | 40 | +| `by-design` | 25 | +| `below-min-severity` (scanner) | 12 | +| `sample-or-demo` | 2 | +| `domain-noun-collision` | 1 | +| `dependency-currency` (finding 1) | 1 | + +The largest family, `placeholder-secret` (50), is really **one value counted 49 times**: +a CI-only encryption key, prefixed `ci-`, is set in `.github/workflows/tests.yml` and quoted +in 17 files under `docs/plans/`. The fiftieth is `.env.example`, where +`JWT_SECRET=` is empty and the match runs onto the next line. The second family, +`mitigated-in-app` (46), is Semgrep's Django CSRF rule firing 45 times on a FastAPI app's +Jinja templates (below), plus one Jinja rendering that Starlette autoescapes. + +## Patterns observed + +**168 findings, zero defects, and two families explain most of the gap.** Neither is +about this project. Semgrep's `django-no-csrf-token` looks for Django's +`{% raw %}{% csrf_token %}{% endraw %}` in any `
`, so it fires on every +form in a FastAPI app whether or not the app defends against CSRF. Voxbento does: both auth +cookies (`admin_token`, `user_token`) are set `HttpOnly` and `SameSite=Lax`, which browsers +withhold on cross-site POSTs; the admin router's writes are POST (39 routes) or DELETE (1); +and the developer dashboard adds a double-submit token checked with `secrets.compare_digest`. +One limit on that verdict: `Lax` does not cover top-level GET navigations, and some GET +handlers set cookies or write rows (logout and email-link redemption among them). Those sit +outside what this rule inspects and were not traced one by one here. The other family is +Gitleaks counting a single test fixture once per document that quotes it. + +**The defaults fail closed.** The shipped `docker-compose.yml` falls back to +`SECRET_KEY=change-me` — the kind of line that is usually the headline of a scan like this. +Here it cannot produce a running production portal: the only signing key the code uses is +`effective_jwt_secret` (`JWT_SECRET`, else `SECRET_KEY`), and the FastAPI lifespan calls +`validate_production_secrets()` at startup (`fastapi_app.py:45`), which refuses to boot on +a known-weak value unless `DEBUG` is on. `DEBUG` defaults to off, and the guard has its own +tests. Third-party provider keys are encrypted at rest under a separate +`API_KEY_ENCRYPTION_KEY` (a `MultiFernet` over a comma-separated key list, so keys rotate +without breaking stored values), and the embeddable player's `postMessage` listener checks +the origin allow-list, when one is configured, before it will play, pause or change volume. + +**The hardening is written down.** `docs/plans/` holds numbered implementation plans, +and several are security hygiene: `005-secret-key-startup-guard`, +`008-admin-access-control-dedup`, `015-debug-default-false`, `016-listener-rate-limit`. +That is the startup guard above, arriving as a plan with tests rather than a patch after an +incident. The same folder is also why Gitleaks reports 47 of its 50 hits: plans quote the +CI configuration they change. + +**The drift is where automation does not reach.** 32 Dependabot pull requests were merged +in the 60 days before the scan, so direct dependencies move daily. What lags is the transitive +layer (Starlette arrives through FastAPI) and the one ecosystem Dependabot is not configured +for (the docs site's `npm` lockfile). One thing worth adding that no scanner flags: voxbento +has no `SECURITY.md`, and GitHub private vulnerability reporting is off, so the next person +with a real finding has no stated private channel. A short `SECURITY.md` naming one, or +enabling private reporting, would close that gap before it is needed. + +## Notes on the tool + +- **Framework-blind CSRF rule.** `django-no-csrf-token` went 45 for 45 false on a FastAPI + app. The discriminating facts (cookie `SameSite`, form method, a double-submit check) + live in Python the template rule never reads. Backlog: gate the rule on a Django + dependency being present, or downgrade it to `info` when none is. +- **One secret, 49 findings.** Gitleaks reports a location, not a secret, so a single + fixture quoted across planning documents surfaced as 49 high-severity findings. Backlog: + fingerprint Gitleaks hits by secret hash and report one finding with N locations. +- **The partial-coverage advice still assumes timeouts.** The Semgrep meta finding says to + "re-run Semgrep with a higher `--timeout`" although zero rules timed out and all 40 errors + were parse errors, which no retry fixes. It was first noted on 2026-09-13 and is still + open; the recommendation should key on the error type. +- **The `uv.lock` gap again.** pip-audit read the `pyproject.toml` floors and not the + shipped lockfile; Trivy's lockfile read carried the dependency result, as on the last + several scans. The meta finding made the gap visible, which is its job. + +## Disclosure timeline + +- 2026-09-28 — scan run at `81a6bbf` (`dev`) +- 2026-09-28 — curated: no first-party finding, so the bar for filing upstream was not met + and nothing was filed +- 2026-09-28 — this page published + +## Reproduce + +```bash +git clone https://github.com/fossasia/voxbento /tmp/scan-target +python scanner/run_scan.py --repo /tmp/scan-target --reports-dir ./reports/fossasia-voxbento --min-severity medium +``` + +--- + +## More from this series + +- **Previous scan:** [superdesigndev/treg](superdesigndev-treg.html) — 2026-09-24, 1 real — withheld +- [Every scan in the series]({{ '/' | relative_url }}) — 112 repositories, newest first +- [Where a maintainer shipped a fix]({{ '/fixed' | relative_url }}) — the 26 that resolved +- [Scans that found nothing]({{ '/clean' | relative_url }}) — published as they were diff --git a/docs/scans/superdesigndev-treg.md b/docs/scans/superdesigndev-treg.md index 427236e..00db5ce 100644 --- a/docs/scans/superdesigndev-treg.md +++ b/docs/scans/superdesigndev-treg.md @@ -184,7 +184,8 @@ python scanner/run_scan.py --repo /tmp/scan-target --reports-dir ./reports/super ## More from this series +- **Next scan:** [fossasia/voxbento](fossasia-voxbento.html) — 2026-09-28, 0 first-party - **Previous scan:** [can4hou6joeng4/boss-agent-cli](can4hou6joeng4-boss-agent-cli.html) — 2026-09-23, 1 real — withheld -- [Every scan in the series]({{ '/' | relative_url }}) — 111 repositories, newest first -- [Where a maintainer shipped a fix]({{ '/fixed' | relative_url }}) — the 25 that resolved +- [Every scan in the series]({{ '/' | relative_url }}) — 112 repositories, newest first +- [Where a maintainer shipped a fix]({{ '/fixed' | relative_url }}) — the 26 that resolved - [Scans that found nothing]({{ '/clean' | relative_url }}) — published as they were diff --git a/docs/work-with-me.md b/docs/work-with-me.md index 090810e..27ecf3d 100644 --- a/docs/work-with-me.md +++ b/docs/work-with-me.md @@ -1,7 +1,7 @@ --- layout: default title: Work with me — AI agent & LLM security review -description: "Independent security review of AI agents, MCP servers and LLM applications. 111 public scans and 26 confirmed fixes as the proof; first-look engagements from $750." +description: "Independent security review of AI agents, MCP servers and LLM applications. 112 public scans and 26 confirmed fixes as the proof; first-look engagements from $750." --- # Work with me @@ -12,7 +12,7 @@ If you're shipping an agent framework, an MCP server, a RAG pipeline, or anythin ## The proof is public -Everything I'd do for you, I've done in the open — [111 curated scans]({{ '/' | relative_url }}) of well-known AI/agent projects, with **26 confirmed fixes** where maintainers acted on the findings, and a documented methodology behind each one. +Everything I'd do for you, I've done in the open — [112 curated scans]({{ '/' | relative_url }}) of well-known AI/agent projects, with **26 confirmed fixes** where maintainers acted on the findings, and a documented methodology behind each one. - **Real findings, not scanner noise.** A typical scan starts at hundreds of raw findings and ends at a handful that matter — because the rest are false positives, by-design patterns, or vendored third-party code. The curation is the value. - **Reachability over rule-count.** A `subprocess(shell=True)` that's safe under operator trust can become a multi-tenant sandbox escape once you trace who reaches it — and a scary-looking SQL string can be fully gated by a five-character allowlist. The verdict goes wherever the *code* goes, which means reading the code, not the rule.