Skip to content

v0.4.2 + v0.4.3: fix two independent adversarial audits (Kimi v2 + v3) - #1

Merged
deva0x merged 3 commits into
mainfrom
fix/v0.4.2-audit-fixes
Jul 25, 2026
Merged

v0.4.2 + v0.4.3: fix two independent adversarial audits (Kimi v2 + v3)#1
deva0x merged 3 commits into
mainfrom
fix/v0.4.2-audit-fixes

Conversation

@deva0x

@deva0x deva0x commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Publishes two audit-fix rounds that were committed locally but never pushed (origin/main is 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 blocked read() forever, inside take_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:

Attack is_file() guard only safe_read_*
2GB sparse plist (instant, ~0 disk) 4111 MB peak RSS, 2.17s 62 MB, 0.01s
symlink swapped to a FIFO after the check hangs again returns None, no hang

All collector reads now go through safe_read_bytes/safe_read_text: O_NONBLOCK, S_ISREG verified 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 put authoriz in the always-show bucket to keep AuthorizedKeysFile visible) — whole-word authorization is now HARD while the Authorized* 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, so PASSWD=<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 correct sudo undo hint · apps in ~/Applications are trust-checked again, and bare_key() restores "why" attribution for (cask)/(snap)/(flatpak) keys · wrong-type labels.json no longer crashes since mark or prune_snapshots · geteuid/uname no longer called at import · big-file state-dir exclusion is a path-prefix match, not a substring.

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).

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.sh guards. See CHANGELOG.md.

How it was proven

  • 129 tests pass; all 4 CI smoke steps pass locally; py3.9 syntax checked.
  • Every finding reproduced pre-fix and re-verified post-fix (hang tests are subprocess-isolated; the SIGALRM test harness is itself self-checked — it does catch an unguarded read wrapped in except Exception).
  • Snapshots from the previous commit vs this code are byte-identical on a real machine (13 collectors + all blobs, 0 errors), so the read-path rewrite altered no output.
  • Two fixes confirmed live: an ad-hoc-signed app in ~/Applications now escalates to RED, and a cask install now shows its brew install line.

Known behaviour changes

  • Locally-built apps in ~/Applications can now escalate to RED (they were never trust-checked before).
  • One-time Linux churn: /etc/xdg/autostart entries appear once as removed+added as their keys gain a (system) tag.

Not fixed, deliberately

  • ~/.netrc is still untracked: redact() doesn't cover its machine/login/password shape, 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 hostile PATH already implies code execution as the user).

🤖 Generated with Claude Code

deva0x and others added 3 commits July 25, 2026 00:29
…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>
@deva0x
deva0x merged commit 9efbe15 into main Jul 25, 2026
6 checks passed
@deva0x
deva0x deleted the fix/v0.4.2-audit-fixes branch July 25, 2026 07:10
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