fix(guards): read .gitignore like git, allowlist settings.json keys, refuse workspaces and bunfig.toml

Three findings on tools/security_guards.py and tools/install_portal_clis.py.

GHSA-rv99-7xf2-8f4p (Martin Vlach, @Huge): the gitignore guard stripped
both ends of each line, but git drops only unescaped trailing whitespace,
so " salary_data.json" satisfied the required-rule check while git had
stopped ignoring the file. Only the root file was read, though a
.gitignore in a subdirectory outranks it, so a negation there re-included
personal data with CI green. Lines are now read as git reads them, and
every .gitignore below the root (outside .git and node_modules) is checked
for negations; an intentional one is allowlisted as "<dir>/.gitignore:!p".

GHSA-cqcv-crx7-x7q7 (@folovco, @Huge): the guard checked allow, ask, deny
and hooks and ignored every other key, so env, apiKeyHelper, a statusLine
command, enableAllProjectMcpServers, permissions.defaultMode or
additionalDirectories passed CI. Any top-level key outside
ALLOWED_SETTINGS_KEYS and any permissions key outside
ALLOWED_PERMISSIONS_KEYS now fails.

GHSA-wq7j-x5jq-pjrw (@folovco, @Huge), on top of #718's --ignore-scripts
(cherry-picked, rudycelekli): the installer and check 3 now refuse a
manifest that declares workspaces (whose packages bring their own
lifecycle scripts; a workspace postinstall ran under Bun 1.3.10) or that
has a bunfig.toml beside it (which reconfigures the install). The
advisory's workspace proof of concept is now skipped before bun runs.

Twelve new tests, all failing before this change; SECURITY.md, README and
SETUP describe the new checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mads LorentzenandClaude Fable 5.1 authored and Mads Lorentzen committed 2026-10-27 16:14:35 +01:00
1 parent 2b8803f777
commit 0bcfd75e60
8 files changed
+306 -12

No files matched your search

+13 -4
View File
@@ -13,10 +13,12 @@ One command for every shell, since Python is already a prerequisite - the
README previously carried a PowerShell and a Bash loop, each with its own
hand-copied list of portal names.
A manifest that declares an npm/bun lifecycle script or trustedDependencies is
skipped and reported instead of installed: those are the hooks that run code
during `bun install`, and tools/security_guards.py forbids them in-tree. A
borrowed portal that carries one gets read by a human before it runs.
A manifest that declares an npm/bun lifecycle script, trustedDependencies or
workspaces, or that has a bunfig.toml beside it, is skipped and reported
instead of installed: those are the doors that run code or reconfigure the
install during `bun install`, and tools/security_guards.py forbids them
in-tree. A borrowed portal that carries one gets read by a human before it
runs.
Every install also passes --ignore-scripts, including on older Bun releases
that run a built-in list of dependency hooks without trustedDependencies.
@@ -58,6 +60,13 @@ def unsafe_reason(cli_dir: Path) -> str | None:
return f"declares lifecycle script(s) {bad}"
if "trustedDependencies" in data:
return "declares trustedDependencies"
if "workspaces" in data:
# Workspace packages carry their own lifecycle scripts, which the check
# above never reads; a workspace postinstall ran under Bun 1.3.10.
return "declares workspaces"
if (cli_dir / "bunfig.toml").exists():
# Reconfigures the install itself: registry, scopes, trusted packages.
return "carries a bunfig.toml"
return None