v0.4.2 + v0.4.3: fix two independent adversarial audits (Kimi v2 + v3) - #1
Merged
Conversation
…rial passes
Second independent (Kimi) audit of the v0.3.1/v0.4 code found that the prior fix
round and the Linux backends had introduced new bugs. Fixed all of them, then ran
two further independent adversarial passes (diff review + full-tool audit) that
caught 8 more issues — including two regressions this very batch introduced. Every
fix carries a regression test; suite 42 -> 87, gate green on macOS.
redact() (the highest-risk function — a finding every round):
- No longer quadratic: a long attacker-plantable rc-file line stalled the unattended
digest for minutes (8.4s@20KB -> 0.5ms); key-name runs bounded {0,64}.
- No longer hides sshd/sudoers directives NOR leaks secrets. A key that NAMES a
credential (password=, SSHPASS=, _auth=) redacts its value unconditionally (incl.
values starting with / $ ~ — base64 tokens, crypt/shadow hashes); a key that merely
CONTAINS a directive name (AuthorizedKeysFile, incl. the default relative-path form)
keeps its value visible so a malicious change stays surfaced.
- PEM/key bodies masked on removed (-) diff lines too; ~/.curlrc user:pass masked.
clean(): strips lone UTF-16 surrogates + U+2028/U+2029; keeps TAB (can't forge a line).
Linux persistence now content-fingerprints (parity with macOS plists): XDG autostart,
systemd effective ExecStart (via systemctl show, catches drop-in overrides), init.d.
Proxy reads system config not the process env. Sensitive-file coverage extended to
/etc/sudoers.d, cron.{daily,hourly,weekly,monthly}, spool, ld.so.preload.
Robustness: _write_private uses mkstemp (stale-temp/PID-reuse safe); `ignore` first-run
makes 0700/0600 state; tilde() collapses only a leading $HOME; --json surfaces the
skip note; parse_when survives absurd windows; install.sh quotes ExecStart and won't
abort under set -e on a headless box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…udit missed Third independent adversarial audit re-verified every v0.4.2 fix as genuine and found one new High. Every finding was reproduced by execution BEFORE fixing, and each fix has a regression test (suite 90 -> 129). High — a plantable file could silently kill the daily digest: - Four collectors (LaunchAgents plists, XDG .desktop, both browser-extension manifest readers) read glob-matched paths with no regular-file check, so a FIFO or a symlink to /dev/zero in a user-writable monitored dir blocked read() forever, inside take_snapshot(), before any output/snapshot/notification. All 5 planted cases hung pre-fix (killed at 5s) and complete post-fix. - Found while attacking that fix (NOT in the audit): a type check bounds nothing. A 2GB *sparse* plist drove 4.1GB peak RSS (measured), and a symlink swapped to a FIFO after the check re-opened the hang. All collector reads now go through safe_read_bytes/safe_read_text: O_NONBLOCK, S_ISREG on the FD (TOCTOU-proof), 8MB cap, history read from the tail at a line boundary. 2GB plist -> 62MB/0.01s. Medium: - `Authorization: <token>` printed in cleartext (v0.4.2 put `authoriz` in the always-show bucket to keep AuthorizedKeysFile visible). Whole-word `authorization` is now HARD; the Authorized* sshd directives stay visible. - redact() per-line cost is now bounded absolutely: a linear pre-filter generated from the SAME keyword constant as the matcher (so they cannot drift) plus a 4KB input cap. 40KB keyword-free line 128ms -> 0.4ms; 400KB -> 0.4ms. Low: - sudoers `PASSWD:` tag no longer hides the command list it prefixes (value-shape gated, so `PASSWD=<secret>` still redacts). - XDG autostart keys no longer collide across dirs (a system entry could hide a planted user entry); system entries tagged ` (system)` with a correct sudo undo hint. One-time Linux churn as those keys change. - Apps in ~/Applications are trust-checked again (the disambiguating tag made _enrich build a path that never exists, so an unsigned app never hit RED); bare_key() also restores "why" attribution for (cask)/(snap)/(flatpak) keys. Both verified live on a real machine. - Wrong-type labels.json no longer crashes `since mark` (or, as found here, prune_snapshots); geteuid/uname no longer called at import (win32 notice); big-file state-dir exclusion is a path-prefix match, not a substring; Linux browser-ext collector skips the Temp dir. Docs: SECURITY.md gains an explicit threat model — a process running as you can tamper with baselines in ~/.local/state/since and erase its own tracks, and helper binaries are PATH-resolved (the daily job's minimal PATH is unaffected). Verified: 129 tests pass; all 4 CI smoke steps pass locally; py3.9 syntax checked; and snapshots from HEAD vs this code are byte-identical on a real machine (13 collectors + all blobs, 0 errors), so none of the read-path changes altered output. Co-Authored-By: Claude <noreply@anthropic.com>
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.
Publishes two audit-fix rounds that were committed locally but never pushed (
origin/mainis still at v0.4.1).v0.4.3 — third independent audit (Kimi v3)
The audit re-verified every v0.4.2 fix as genuine and found one new High. Every finding was reproduced by execution before fixing; each fix has a regression test (suite 90 → 129).
High — a plantable file could silently kill the daily digest. Four collectors (LaunchAgents plists, XDG
.desktop, both browser-extension manifest readers) read glob-matched paths with no regular-file check, so a FIFO — or a symlink to/dev/zero— in a user-writable monitored directory blockedread()forever, insidetake_snapshot(), before any output, snapshot or notification. All 5 planted cases hung pre-fix (killed at 5s) and complete post-fix.High (found while attacking that fix — not in the audit). A type check bounds nothing:
is_file()guard onlysafe_read_*None, no hangAll collector reads now go through
safe_read_bytes/safe_read_text:O_NONBLOCK,S_ISREGverified on the file descriptor (so the check-then-open race cannot be won), 8MB cap, history read from the tail at a line boundary.Medium.
Authorization: <token>printed in cleartext (v0.4.2 putauthorizin the always-show bucket to keepAuthorizedKeysFilevisible) — whole-wordauthorizationis now HARD while theAuthorized*sshd directives stay visible.redact()per-line cost is bounded absolutely: a linear pre-filter generated from the same keyword constant as the matcher (so they cannot drift) plus a 4KB input cap — 40KB keyword-free line 128ms → 0.4ms.Low. sudoers
PASSWD:tag no longer hides the command list it prefixes (value-shape gated, soPASSWD=<secret>still redacts) · XDG autostart keys no longer collide across dirs, where a system entry could hide a planted user entry, and system entries get a correctsudoundo hint · apps in~/Applicationsare trust-checked again, andbare_key()restores "why" attribution for(cask)/(snap)/(flatpak)keys · wrong-typelabels.jsonno longer crashessince markorprune_snapshots·geteuid/unameno longer called at import · big-file state-dir exclusion is a path-prefix match, not a substring.Docs.
SECURITY.mdgains an explicit threat model: a process running as you can tamper with baselines in~/.local/state/sinceand erase its own tracks, and helper binaries arePATH-resolved (the daily job's minimalPATHis unaffected).v0.4.2 — second independent audit (Kimi v2)
redact()no longer conceals the sshd/sudoers attacks it exists to surface nor leaks the secrets it should mask;redact()no longer quadratic (8.4s @ 20KB → 0.5ms); PEM bodies masked on removed (-) lines; Linux autostart fingerprinted by content hash. Plus Linux proxy/env fixes, surrogate handling,install.shguards. SeeCHANGELOG.md.How it was proven
except Exception).~/Applicationsnow escalates to RED, and a cask install now shows itsbrew installline.Known behaviour changes
~/Applicationscan now escalate to RED (they were never trust-checked before)./etc/xdg/autostartentries appear once as removed+added as their keys gain a(system)tag.Not fixed, deliberately
~/.netrcis still untracked:redact()doesn't cover itsmachine/login/passwordshape, so adding the file without the redaction would create a leak.PATH-resolved subprocesses are documented as an accepted risk rather than code-fixed (absolute paths vary by distro; a hostilePATHalready implies code execution as the user).🤖 Generated with Claude Code