Skip to content

fix(security): refuse more credential places on send, and write output-folder files exclusively - #1234

Merged
leemour merged 2 commits into
mainfrom
fix/upload-credential-paths
Oct 14, 2026
Merged

leemour merged 2 commits into
mainfrom
fix/upload-credential-paths

Conversation

@leemour

@leemour leemour commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Task

SEC-48 (medium) and FIND-1299 (low) from the 2026-10-14 security run.

Why

  • After Restore file, document and output compatibility #833 narrowed the upload refusal to a fixed list (as the 2026-10-10 ruling says: hidden paths alone are not a reason to refuse), the list missed well-known credential places. A model talked into "send me that file" could attach ~/.netrc, ~/.git-credentials, ~/.config/gh/hosts.yml, ~/.docker/config.json, ~/.kube/config, ~/.claude/.credentials.json, ~/.codex/auth.json, keyrings, browser cookies, or another WireCat app's session. The messages_send tool description still claimed hidden files are refused, which is no longer true.
  • messages download progress (download-command.ts) and the export manifest (export-folder.ts) were written to a fixed .tmp / .next name without wx, inside a folder the owner chose. A link planted at that name would be written through. The ruling keeps exclusive temporary writes.

What

  • refusedPlace (src/sends/upload.ts) refuses these places:
    • Single path parts: .netrc, _netrc, .git-credentials, .azure, .kube, .password-store and .mozilla, in addition to the existing entries.
    • Runs of parts: .config/{git/credentials,gh,gcloud,google-chrome,chromium,microsoft-edge,BraveSoftware}, .docker/config.json, .claude/.credentials.json, .codex/auth.json, .local/share/keyrings, Library/{Keychains,Cookies,Safari}, the macOS Application Support folders of Chrome, Chromium, Edge, Brave and Firefox, the Windows AppData folders of Chrome, Chromium, Edge, Brave and Firefox, Microsoft/{Credentials,Protect}, and AppData/Roaming/{gcloud,GitHub CLI}.
  • Every WireCat app's config, state and cache folders are refused, not only the running app's: max-cli, tg-cli, cli-memo, drive-cli, zm (zoom), and the legacy cli-messaging and cli-common. Each respects its <PREFIX>_*_DIR overrides.
  • The shared WireCat folder (wirecatDirectory: the store, its backups, the models) is refused as a whole, as max-cli's own list did.
  • refusedPlace is exported from @wirecat/cli-core/sends, so max-cli can drop its duplicate list and call this one.
  • The refusal names --allow-any-file but no command, because max bot uploads put and tg's bot api @path fields reach it too.
  • parity.json plans --allow-any-file on bot api itself for tg (today it is on each of the 33 file-carrying operations). The row is in: [] with a planned note, so neither CLI's parity check changes until tg adopts it.
  • On darwin and win32, names and folders compare case-insensitively. refusedPlace takes an optional platform argument for this.
  • Ordinary hidden folders still send. The tests cover .worktrees, .cache, .incoming, .config/nvim, .docker/daemon.json, .claude/settings.json, .codex/config.toml and .local/share/fonts.
  • The messages_send description now names what is refused and says other hidden files are sent. The refusal messages are reworded to match.
  • FIND-1299: both writes now go through cli-base's writeSecurely, which writes an unguessable temp name with flag: "wx", then renames.
  • Left out on purpose: the --allow-any-file help text still says "this CLI's own folders". Rewording it would also need the generated docs regenerated in max and tg; it is not wrong, only incomplete.

Testing

  • upload.test.ts: one case per new entry (32 entries), a test that every other WireCat app's folders are refused, a test that .SSH, .Netrc, .ENV, .Config/GH and the app's own STATE folder are refused on darwin and win32 but .SSH is not on linux, and the hidden-folder allow list. A test that the WireCat folder (with a store backup) is refused and a neighbour folder is not. A test that the refusal names the flag but no command.
  • temporary-writes.test.ts: with a link planted at the old .tmp / .manifest.json.next name, the link's target is untouched and the real file is written.
  • pnpm typecheck and pnpm lint pass. vitest run src/services src/sends src/attachments src/mcp src/cli/messenger passes: 1881 tests. After the rebase and the follow-ups, 580 tests in src/sends src/attachments src/mcp src/services/attachments and messenger.test.ts pass. These ran with TMPDIR on /home, because the local root disk is full.

Reviewed against the review checklist — §A for a docs PR, §B for code.

🤖 Generated with Claude Code

@leemour
leemour force-pushed the fix/upload-credential-paths branch from 84bb7f8 to 9a03dd3 Compare October 14, 2026 21:45
leemour and others added 2 commits October 15, 2026 00:00
…t-folder files exclusively

The upload refusal list named only a few credential folders. A talked-into
agent could still attach ~/.netrc, ~/.git-credentials, gh/gcloud/docker/kube
tokens, Claude and Codex credentials, keyrings, browser profiles, or another
WireCat app's session (SEC-48). These are added as path parts or runs of
parts, every WireCat app's folders are refused, and on macOS and Windows the
names compare case-insensitively. Ordinary hidden folders still send, as the
2026-10-10 ruling requires; the messages_send description now says so.

Download progress and the export manifest replaced a fixed `.tmp`/`.next`
name without `wx` (FIND-1299); both now go through writeSecurely, an
unguessable name opened exclusively, then renamed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ommand in the refusal

max-cli and tg-cli can call the shared check instead of keeping their own
list. The WireCat folder holds the store's backups. Bot uploads and tg's
`bot api` @path fields reach the refusal, so it names the flag alone. The
parity manifest plans --allow-any-file on tg's `bot api` itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@leemour
leemour force-pushed the fix/upload-credential-paths branch from 9a03dd3 to fa79ae5 Compare October 14, 2026 22:03
@leemour
leemour merged commit b474be6 into main Oct 14, 2026
2 checks passed
@leemour
leemour deleted the fix/upload-credential-paths branch October 14, 2026 22:04
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