feat(webapp): refuse to share a file that looks like it holds credentials (BEA-111) (#137)

Minting a share link ran zero content checks: a file holding an AWS-shaped
key became a public URL on one click, and the CLI printed nothing but the
link. handleShareCreate now reads the first 1 MiB and runs six anchored
rules between the synced-path check and Shares.Create, answering 409 with
rule ids and line numbers unless the request carries confirm: true.

The matched text never leaves scanSecrets — not into the body, not into a
log line. TestShareSecretNeverEchoed greps both for the planted string,
because a 409 body is the easiest place in this codebase to leak it.

Both callers carry the override, since the gate alone would turn any false
positive into a hard block with no way out: `bdrive share --force`, and the
browser's Share-anyway dialog on modalConfirm (no new component). A path
that already has a live link skips the scan — its content is public
already, so withholding the URL protects nobody — but alreadyPublic drops
links whose creator left the org, since those 404 at /s/ and would
otherwise wave a secrets file straight through.

A failed blob read is 503, not a silent pass: the repo's "degrade rather
than fail" posture is for sync cycles, and a check that skips itself on a
storage hiccup is the false confidence this exists to remove.

Every user-facing string says the file was checked at the moment you shared
it. A link serves the file's LATEST content forever, so a key written into
an already-shared file is never caught — that open loop stays open, and the
copy is the only thing stopping v1 from claiming otherwise.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Snow Lee (Sungwon)
2026-08-11 04:33:05 +09:00
committed by GitHub
co-authored by Claude Opus 5
parent 594a027c15
commit d3d92bf904
18 changed files with 701 additions and 133 deletions
@@ -91,8 +91,11 @@ scripts can't touch hub sessions and a scraper can't turn your hub into a CDN.
:::caution
Any org member can mint links, and a link is public to whoever holds the URL.
Don't put secrets in a synced folder. Note also that a LAN-bound hub means
LAN-only links.
Before minting, the hub scans the first 1 MiB of the file for
credential-shaped strings and refuses (`--force`, or **Share anyway** in the
UI, overrides it) — but that check runs *at the moment you share*, and a link
serves the file's latest content forever, so don't put secrets in a synced
folder. Note also that a LAN-bound hub means LAN-only links.
:::
## Who wrote what
@@ -231,3 +231,12 @@ Regardless of configuration:
the seeded default covers the common cases. But treat it as hygiene, not a
security control: any org member can mint a public link for any synced file.
Secrets belong in a secret manager, not in a folder you hand to agents.
Minting a share link is the one place the hub reads the bytes first: it scans
the **first 1 MiB** for credential-shaped strings (AWS access key, private key
block, GitHub/Slack/GitLab/OpenAI token) and refuses, naming the rule and the
line but never the matched text. `bdrive share --force` — or **Share anyway**
in the web UI — overrides it. Know what that check does *not* cover: it runs
**at the moment you share**, and a link serves the file's latest content
forever, so a key written into an already-shared file is never caught. It is a
last-line backstop, not a reason to sync a folder you wouldn't otherwise.
+1 -1
View File
@@ -20,7 +20,7 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
| `bdrive grep <pattern> [folder]` | Search the text **inside** the files a project syncs. `pattern` is a Go RE2 regexp, or a literal string with `-F`. `-i` ignores case, `-l` prints matching paths only, `-n` caps the lines printed (default 200, `0` = all). Pure read: no daemon, no lock, no network |
| `bdrive forget <path>...` | Stop syncing a path and remove it from the hub. Adds the rule to `.bdriveignore` (which syncs) and prunes in one step. Local files are never touched, here or on teammates' devices |
| `bdrive url [path]` | Internal hub link for a file or folder — sign-in and membership required. `--sync` pushes first; no argument gives the project home. Computed locally |
| `bdrive share <file>` | Public URL for a synced file. `--list`, `--revoke`, `--expires` (the hub's Share dialog can also set an expiry on an existing link) |
| `bdrive share <file>` | Public URL for a synced file. `--list`, `--revoke`, `--expires` (the hub's Share dialog can also set an expiry on an existing link). Refuses a file whose first 1 MiB holds credential-shaped strings — `--force` shares it anyway |
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing |
| `bdrive hooks [install\|uninstall]` | Register turn-boundary sync hooks in each detected agent platform's user config — once per machine, covering every folder. Run automatically by `bdrive init`; idempotent; `--agent` overrides detection. `uninstall` removes only BearDrive's own hook entries |
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads for the hub's read heatmap. Registered by `bdrive hooks install` |