Skip to content

fix(security): audit security findings — traversal, secrets, supply chain#497

Merged
markhayden merged 7 commits into
mainfrom
fix/security-audit
Jun 13, 2026
Merged

fix(security): audit security findings — traversal, secrets, supply chain#497
markhayden merged 7 commits into
mainfrom
fix/security-audit

Conversation

@markhayden

Copy link
Copy Markdown
Owner

Summary

First execution PR from the audit (.claude/specs/audit-2026-06/REPORT.md, plan in #496). Six independent findings, one revertable commit each — any single commit can be git reverted cleanly.

  1. Delete dead legacy plugin installersrc/core/plugin-installer.ts carried a textbook shell injection (execSync string interpolation of caller input), kept re-adoptable only by its own test. Both deleted.
  2. Avatar route path traversalGET /api/agents/avatar?id=../… resolved outside the agents root. Safe-slug validation, proven by a failing test first.
  3. Plugin-settings route id guard — defense-in-depth: the one plugin-id route missing the canonical PLUGIN_ID_RE check (reused from @bakin/core/plugins/manifest).
  4. Ledger idempotency rows are content-free — billed-image dedup rows persisted prompt text + provider commentary, violating the "coordination facts only" invariant. Stripped at the persistence boundary; replays still return the right asset for $0. Ledger knowledge doc updated.
  5. Antfly password → secret store — the basic-auth password rode settings.json and GET /api/settings unredacted. Now in 0600 secrets.json under providers.antfly (env ANTFLY_PASSWORD overrides), injected at adapter init; one-time boot migration relocates a legacy value. Adapter package untouched. Search knowledge doc updated.
  6. Github installs rebuild from validated sourcetrustExistingDist deleted; the import validator and the executed artifact now always come from the same source tree. Provenance-verified Whiskit artifact installs are unaffected. (Verified pre-removal that the original constraint — no SDK resolution on consumer machines — no longer holds.)

Verification

  • bun run test (4,972 pass / 0 fail), bun run typecheck, bun run lint — green per commit
  • bun run build — all three platform binaries compile
  • Boot smoke in an isolated BAKIN_HOME: server serves 200, the live antfly migration ran end-to-end (password stripped from settings.json, secrets.json created 0600, /api/settings response clean)

Refs #496.

🤖 Generated with Claude Code

markhayden and others added 7 commits June 11, 2026 22:21
… liability)

src/core/plugin-installer.ts built a `git clone` shell string from caller
input (execSync interpolation) — a textbook command injection kept alive
only by its own test. The production installer (api/plugins/install.ts +
github-source-cache) supersedes it with argv-array git, ref validation,
and path containment. Audit finding: sec-injection-supplychain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/agents/avatar joined the query id straight into a filesystem
path — `?id=../<dir>` resolved outside the agents root (bounded by the
fixed avatar.jpg suffix, but still the one route skipping the containment
discipline every other id-consuming route follows). Safe-slug regex →
400; traversal proven by a failing test first. Audit finding: sec-traversal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET/PUT /api/plugin-settings/{pluginId} used the last path segment
unvalidated as a filename stem. Defense-in-depth: Node URL normalization
already blocks traversal, but this was the one plugin-id route without
the canonical PLUGIN_ID_RE guard. Reuses the exported regex from
@bakin/core/plugins/manifest. Audit finding: sec-traversal (P2 pull-in).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Billed-image dedup rows stored the full ExecToolResult — including up to
500 chars of prompt text and the provider's commentary — violating the
ledger's "coordination facts only, never content" invariant. prompt and
providerText are now stripped at the persistence boundary; asset identity
+ promptHash remain, so replays still return the right asset for $0 and
the caller's first-run result is unchanged. Existing rows written before
this commit retain their content (single-user box; new writes hold the
invariant). Audit finding: sec-secrets-ssrf (P2 pull-in).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
settings.json carried search.settings.auth.password and GET /api/settings
returned it unredacted — contradicting the secret store's own invariant
(secrets never ride a settings broadcast/export). The password now lives
in the 0600 secrets.json under providers.antfly (env ANTFLY_PASSWORD
overrides), settings keep only the username, and createAppServices
injects the resolved credential at adapter init. A one-time boot
migration relocates a legacy password and strips it from settings.json.
The antfly adapter is untouched (boundary holds). Partial settings-module
mocks across tests/ gained the resetSettingsCache export app-services now
imports. Audit finding: sec-secrets-ssrf (P2 pull-in).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p dist trust

trustExistingDist let github installs/upgrades and the boot sweep
activate a shipped dist/ that was never rebuilt from the import-validated
source — the validator ran against source while the executed artifact
came from the tarball. The historical justification (consumer machines
couldn't resolve SDK sources for a server rebuild) is gone: Whiskit
builds from the compiled binary via system bun and resolves the SDK
through the plugin's own node_modules. github/local installs now always
build from source; the mtime freshness check remains as a pure cache;
provenance-verified Whiskit artifact installs keep skipping the build
step before this layer. Audit finding: sec-injection-supplychain
(P2 pull-in, verified post-audit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…port

The buildAllUserPlugins lockfile read lost its last consumer with the
dist-trust removal; the health plugin's HealthRepairHandler type import
was already unused on main and blocked the lint gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
markhayden added a commit that referenced this pull request Jun 12, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@markhayden markhayden merged commit d3c99a6 into main Jun 13, 2026
1 check passed
@markhayden markhayden deleted the fix/security-audit branch June 13, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant