The PostToolUse hook read input.tool_output, but live Claude Code delivers the tool result as tool_response — so the indirect-injection scan on MCP tool output silently never fired outside the test harness (which sent tool_output). The hook now reads tool_response with a tool_output fallback for older harnesses/fixtures. +3 tests covering string and object tool_response and the precedence. Found via a live-session check during the v7.8.3 sweep; not one of the 52 MEDIUM-tier findings. Suite 2016/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
Bumps plugin to 7.8.3 (package.json, plugin.json, README version badge) and syncs the documentation surfaces that had drifted: orchestrated scanner count 9/10/13 to 14 (#8; the WFL/workflow scanner was omitted from the reference list), posture categories 13 to 16 (#44), red-team scenarios 64 to 72 (#45), tests badge/prose 1822 to 2013 (#46). Corrects the pathguard hook-table matcher and header comment to Edit|Write (follows the #9 fix) and removes the two dangling ROADMAP.md references (#47; the roadmap is not a public file). Adds the v7.8.3 CHANGELOG entry, README Recent-versions row, and CLAUDE.md highlights. The '23 scanners' total is a separate module count and is left unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#58 the e2e scan-pipeline suite omitted trg/sig/ast from EXPECTED_SCANNERS and no fixture carried trigger-abuse/known-signature/python-taint content, so the three v7.8.0 scanners' surfacing through the aggregate was never asserted; added a fixture (shadowing command name → TRG, webshell → SIG, tainted os.environ→requests.post → AST, python3-guarded) that asserts each appears in the envelope and the rolled-up counts/owasp_breakdown. #59 the SIG cryptominer (SIG-MINER-001/002) and hacktool (SIG-HACKTOOL-001) families and non-base64 decode variants had no test; added family-attribution tests and a rot13-decode assertion. Test-only, suite 2013/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#20 git-forensics' reflog force-push detector had a redundant bare 'reset' term subsuming 'reset:', so any commit subject containing 'reset' tripped it; removed. #22 diff-engine's per-current moved-fallback greedily consumed a baseline candidate a later byte-exact match needed (mislabeling unchanged as new/moved on duplicate fingerprints); a global exact pass now runs before the moved-fallback. #54 memory-poisoning double-reported a 64+ char hex token as both base64 and hex; the base64 check now skips pure-hex tokens.
#56 SARIF output hardcoded driver.version 6.0.0 because the orchestrator called toSARIF() without a version; it now passes the real plugin version from package.json. #50 the VS Code known-malicious blocklist was empty with no explanation (unlike the JetBrains file's 'empty by design' note); added a matching blocklist_note. Suite 2004/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#9 pathguard registered matcher Write only, so an Edit/MultiEdit to an existing protected file (settings.json, .env, .ssh, the hooks themselves) bypassed it entirely; matcher now Edit|Write (the script already reads only tool_input.file_path). #10 the primary trifecta detector's 20-entry window counted marker lines, so accumulated markers scrolled a real leg out (false negative); the window now counts tool-call entries. #13 pre-edit-secrets caught bare provider keys only inside a quoted label assignment; added anchored patterns for Anthropic sk-ant, OpenAI sk-proj, fine-grained github_pat_, Google AIza, and JWT eyJ (minimum-length guarded against prose false positives).
#12 the remote-pipe-to-shell block required a shell immediately after the first pipe, so xargs/sudo/tee/env interposition evaded it and the comment falsely claimed xargs was caught; broadened to reach a shell through intermediate segments while leaving shell-OR fallbacks unblocked. #11 (doc only): knowledge/owasp-skills-top10.md claimed pre-bash-destructive blocks persistence commands — it does not; corrected to mark persistence detection as unimplemented/future (the detector itself is deferred to v8). Suite 2004/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#21 the bash normalizer decoded only \xHH, leaving ANSI-C octal/\u/\U forms literal so canonical rm/curl never surfaced; now decodes all three. #23 file-discovery keyed on extname so .env.local/.env.example (extname .local/.example) were silently skipped; now matches multi-part suffixes. #42 a legitimate leading UTF-8 BOM was flagged HIGH (and the tool's own auto-cleaner refused to strip it); pos-0 BOM now excepted. #52 collapseLetterSpacing used a literal space, letting multi-space/tab spacing evade; now [ \t]+. #55 redact(_,60,0) did slice(-0) and leaked the whole unredacted URL; showEnd===0 now means no tail.
#30 embedded base64 (const x = "<base64>") never satisfied the whole-string decode, so the SIG identity engine never saw it; added decodeEmbeddedBase64 as an OPT-IN param on normalizeForScan (default off — appending a decoded copy would double-count per-match findings, e.g. content-extractor's injection scan) and enabled it only in signature-scanner, which dedups variants. #36 signature-scanner ignored the documented sig.custom_rules_path policy option; now loads+merges custom rules through the same family filter. Suite 2004/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#33 the frontmatter parser collected a block-scalar body (description: |) but did not skip it, so an indented name:/allowed_tools: inside the body re-matched as a top-level key and overrode the real values TRG-shadow and permission checks depend on; the parser now consumes block-scalar bodies as opaque content. #32 block-scalar headers carrying indentation/chomping indicators (|2, >-, |-2) were not recognized, so their bodies never reached the run: injection sink; now matched via a proper indicator/chomping regex.
#43 the B4 actor auth-bypass detector inspected only braced ${{ }} expressions, missing the canonical bare 'if: github.actor == ...' form (Synacktiv Dependabot-spoof false negative); bare if: expressions now emit a synthetic event the detector reads. Suite 2004/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#24 the HTML-obfuscation injection patterns had overlapping unbounded runs plus a required closing quote, so a non-closing input backtracked O(N^2) (~28.7s at the 512KB cap); quantifiers bounded, pathological input now 4ms. #53 mcp-live-inspect buffered MCP-server stdout via readline with no cap, so a hostile stdio server could exhaust memory / throw an uncaught RangeError; replaced with manual line buffering capped at 4MB that rejects pending RPCs and destroys stdout. #31 vsix-fetch's same-host redirect follower had no depth cap (loop hang); added depth>=5 cap mirroring the sibling fetcher.
#25/#51 mcp-description-cache and skill-registry wrote JSON via bare writeFileSync (non-atomic: concurrent load-modify-save loses updates, a torn read silently yields an empty registry); both now write a temp file then renameSync. Suite 1931/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#38 toxic-flow matched trifecta-leg keywords with bare includes(), so substrings ('url' in 'curl', 'key' in 'monkey', 'auth' in 'author') fabricated CRITICAL trifectas on benign components; now word-boundary matched. #40 TRG-broad fired HIGH on a bare any/all/every anywhere ('fix any lint errors'); the universal-claim regex now requires genuine universal phrasing. #41 TRG-baiting substring-matched ('any file' in 'many files'); now boundary-anchored. #39 the broad-name list missed multi-char generic names (helper/assistant/auto/general/agent/tool); widened coherently so it does not reintroduce #40. #57 the '(recovered from obfuscation)' label compared raw against a lowercased normal form, firing on any uppercase char; now gated on an explicit decode-changed flag.
#26 (same file) getPolicyValue used 'key in sectionObj' with no type guard, so a scalar section override in policy.json (e.g. {"injection":"block"}) threw an uncaught TypeError; now guarded to fall back to the default. Suite 1931/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
#18 dep-auditor called 'pip audit' (no such subcommand) so Python CVE detection was a permanent silent no-op; now spawns 'pip-audit' with an argv array. #19 it audited the scanner HOST's env with results mislabelled to the target's requirements.txt; now audits the target via -r and skips cleanly when absent.
#14 the offline npm blocklist was skipped for bare/range/tag installs because it used the (null) parsed-spec version; now re-checks the resolved version before the OSV network path. #15 non-hoisted nested lockfile keys derived the wrong package name (leading-only node_modules/ strip); now strips to the last segment. #48 lockfileVersion-1 nested dependency trees are now walked recursively.
#16/#17 the yarn.lock matcher paired two unassociated whole-file substrings with an unanchored pkg@ (false BLOCK of a legit package) and only matched Yarn Classic quoted versions (Berry known-malware allowed); rewritten as a per-entry parser that associates version to its own entry, anchors the name, and matches both Classic and Berry. Suite 1931/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
py-ast-taint.py Assign handler never removed a name from the tainted set on reassignment, so a source-then-constant/sanitizer rebind (g=os.getenv(); g='safe'; os.system(g), or x=shlex.quote(x)) kept stale taint and fired false AST-CMD-EXEC findings. Assign now clears taint for target names when the RHS is not a source. No cross-expression propagation added — the f-string/concat/alias recall gap (#27) stays deferred to v8.
#28: added fixtures+assertions locking the tainted subprocess/os.system AST-CMD-EXEC sink and the open(...,'w') AST-FILE-WRITE sink. Suite 1931/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
Bumps package.json, .claude-plugin/plugin.json and the README badge to
7.8.2; adds the release entry to CHANGELOG.md, docs/version-history.md,
the README recent-versions table and the CLAUDE.md highlights block.
Also fixes test pollution introduced with the ide-extension regression
suite: its temp roots used an `llmsec-jb-plugin-` prefix, and
jetbrains-parser.test.mjs asserts globally that no `llmsec-jb-*`
directory survives anywhere in tmpdir. The shared prefix made that
assertion fail depending on test order — it passed on the first full run
and failed on the next. Prefix is now `llmsec-nullmanifest-`.
npm test: 1901/1901 green, three consecutive runs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
probe-rm.mjs was self-labelled "Temporary probe — delete after
debugging". It is not a test (no node:test harness, no assertions — it
prints exit codes), it ran against a hardcoded absolute path into the
INSTALLED marketplace copy rather than this repo, and it exercised the
rm-block cases that are now covered properly by
tests/hooks/pre-bash-destructive.test.mjs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
decodeEntities guarded parseInt with Number.isFinite, which bounds
nothing: any numeric character reference above 0x10FFFF produced a
finite integer that String.fromCodePoint rejects with RangeError.
Correction to the review's framing: this does NOT break parsePluginXml's
no-throw contract. The per-field safe() wrapper catches it. The actual
defect is quieter — the affected field is discarded and replaced with
'', with only a warning. A plugin whose <name> carries one out-of-range
reference parses as name: "" while pluginId and every other field
survive, so name-based checks (JetBrains typosquat detection against the
top-plugin list) run against an empty string.
Severity is below the HIGH it was filed as: a document containing a code
point above 0x10FFFF is not well-formed XML, so IntelliJ would reject
such a plugin too — the evasion yields a plugin that does not load. The
fix is still correct and one line of logic.
- isDecodableCodePoint(): integer, >= 0, <= 0x10FFFF.
- Undecodable references are now left literal, matching how lenient
parsers treat unrecognised entities and how this same function already
treats unknown named entities.
Regression test drives parsePluginXml with hex and decimal references
just past and far past the maximum, asserts the no-throw contract still
holds, and pins that valid references, the maximum valid code point, and
named entities still decode.
npm test: 1901/1901 green (1890 + 11 new).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
stripInjection is the remote-scan indirection layer: its `sanitized`
output is what an LLM agent actually reads, verbatim, via
sanitized_content in the evidence package. It scanned two variants of
each file — the raw text and normalizeForScan(text) — but removed
matches with `sanitized.replace(match[0], ...)` against the RAW text
only.
For a match found in the decoded variant, match[0] IS the decoded
string, which by construction does not occur in the raw text. The
replace was therefore a silent no-op: the finding was reported while the
encoded payload was passed to the agent untouched. Every obfuscation the
normalizer exists to defeat — HTML entities, URL encoding, \u escapes,
hex, base64, letter-spacing, Unicode tags — reached the agent intact.
The worst case is the intended one: detection said "critical injection
found" and shipped the injection along with the verdict.
- Pass 1 redacts the whole source LINE whose own normalized form carries
the pattern. Line granularity is deliberate: decoding is not
length-preserving, so decoded match offsets cannot be mapped back onto
the original text.
- Pass 2 keeps the existing literal replacement and finding collection.
- Residual gap, made explicit rather than silent: a payload encoded
across MULTIPLE lines matches whole-text normalization but no single
line, so it cannot be attributed. Those findings now carry
`unstripped: true`. Whole-file redaction was considered and rejected —
normalizeForScan base64-decodes any long blob, so a benign asset could
blank an entire file's evidence.
- stripInjection exported via __testing, and main() is now behind the
standard isMain guard (copied from dashboard-aggregator.mjs) so
importing the module does not execute the CLI. CLI verified unchanged
against the evil-project-health fixture: 7 files, 6 injection
findings, risk_level critical.
This boundary had no direct test coverage before this commit.
npm test: 1890/1890 green (1884 + 6 new).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
The two manifest parsers disagree on how they signal failure:
parseVSCodeExtension returns a bare null, parseIntelliJPlugin returns a
TRUTHY { manifest: null, warnings }. scanOneExtension guarded only the
bare-null form, so every JetBrains failure path — lib/ missing, lib/ not
a directory, lib/ unreadable, no jars in lib/, no jar extractable — fell
through the guard and dereferenced `manifest.hasSignature`.
The resulting TypeError propagated out of scanOneExtension into
mapConcurrent, which awaited fn() with no per-item try/catch, so
Promise.all rejected and the ENTIRE ide-scan aborted. One malformed
plugin directory on disk was enough to take down the scan of every
other installed extension — including, in an audit context, a plugin
that is malformed precisely because it is hostile.
- scanOneExtension: guard is now `!parsed || !parsed.manifest`, and the
parser's own warnings are carried into the result so the reason for
the skip survives instead of being replaced by a generic message.
- unscannableExtension(): the result envelope is extracted so the early
return and the isolation belt below produce the same shape.
- Belt (defense-in-depth): the scanOneExtension call site catches per
extension and yields an unscannable result. mapConcurrent stays
generic — the isolation lives at the call site, not in the helper.
Regression test drives scanOneExtension across all four JetBrains parse
failure paths plus a no-invented-findings assertion.
npm test: 1884/1884 green (1879 + 5 new).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
Rule 4 in isFalsePositive ("test/fixture files intentionally contain
example secrets") matched /(test|spec|fixture|mock|__test__|__spec__)/i
against absPath — the ABSOLUTE path. Any directory name above the scan
root therefore silenced every entropy finding in the entire target: a
repo cloned to a CI workspace, checked out under a parent folder named
`testing`, or scanned from any path with one of those substrings
reported zero secrets and still exited status 'ok'. The failure is
silent — indistinguishable from a clean scan.
- isFalsePositive takes relPath and rule 4 keys off it. relPath was
already computed and passed down to scanFileContent; only the
suppression check was reading the wrong one.
- classifyFileContext keeps using absPath: it reads the basename
extension only, so directory names above the root cannot affect it.
- Rules 1-3 and 5-13 are untouched.
Regression test drives scan() end-to-end against temp roots named
llmsec-test-*, llmsec-spec-*, llmsec-fixture-* and llmsec-mock-*, with a
neutral-root control proving the payload is detectable, plus two
guards that genuine suppression still works (a *.test.mjs file and a
file under a relative tests/ directory).
npm test: 1879/1879 green (1872 + 7 new).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
The BLOCK rule named "Filesystem root destruction" did not block the bare
root target it is named for. The target alternation ended in a shared `\b`,
and a word boundary cannot hold after `/` or `~` at end-of-command.
Measured against the shipped pattern:
bare `/` target NOT blocked `$HOME` target blocked
bare `~` target NOT blocked `/usr` target blocked
`/*` glob target NOT blocked
swapped flags (-fr) NOT blocked
sudo-prefixed NOT blocked
Only targets whose first character is a word character ever satisfied the
assertion, so the rule caught `/etc` but not bare root. These fell through
to WARN (exit 0) — advisory only, command executed.
- Pattern: `\b` moved onto the `$HOME` alternative alone, where it is
meaningful (it ends in a word char, so `$HOMEDIR` is still not
swallowed). Dropped from `/` and `~`. Nothing else changes: `/etc`,
`/home`, `./build` behave exactly as before.
- The old test file encoded this defect as expected behaviour, with a NOTE
claiming the pattern "requires separate flag groups (e.g. -f -r, not -rf
combined)". That diagnosis was wrong — the `/etc` case blocks fine with
merged flags. Comment replaced with the real root cause.
npm test: 1872/1872 green (1865 + 7 new).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
Version bump + release notes for the auto-cleaner command-injection fix
(f083cda). Security patch only; no feature changes. 1865 tests, 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
validateContent() syntax-checked .mjs/.js/.cjs candidates via
execSync(`node --check "${tmpPath}"`), where tmpPath derives from the
untrusted scanned-repo FILENAME. The F-2 guard checks path containment
but never quotes or strips shell metacharacters, so a file named
`x";<cmd>;".mjs` in a scanned repo turned `/security clean` (live is the
documented default) into arbitrary command execution. Verified with a
live PoC before the fix.
- validateContent: spawnSync('node', ['--check', tmpPath]) — no shell.
- CLI orchestrator fallback: same treatment (argv array, explicit
status/error handling instead of execSync's throw-on-nonzero).
- Belt (defense-in-depth): applyFixes now refuses findings whose `file`
carries shell or control metacharacters, reported as skipped.
- Regression tests cover both layers separately, so the belt cannot mask
a re-introduced shell in the sink.
node --test: 1865/1865 green (1863 + 2 new).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
Reword the v7.8.0 release notes to refer to the scanners as TRG/SIG/AST only;
the internal codename is removed from CHANGELOG, CLAUDE.md, README, the
version-history section, and STATE. No behaviour or version change.
Session C of the security-fix track: cut the TRG/SIG/AST release now that the
F-1/F-2/F-3 security gates pass. Pure version-sync — the three scanners shipped
in Steps 1-7 (616e7ff..fc7cf57); no production code changes here.
Version-sync 7.7.2 -> 7.8.0 across every current pointer: .claude-plugin/
plugin.json, package.json, README.md badge, and the CLAUDE.md header + "release
notes" range sentinel. Narrative additions (prior releases kept as history):
- CHANGELOG.md: new [7.8.0] entry (Added: TRG/SIG/AST; Changed: prefix
registration + knowledge/ packaging).
- docs/version-history.md: new v7.8.0 section describing the three scanners.
- README.md "Recent versions": new 7.8.0 row.
- CLAUDE.md: new v7.8.0 highlights paragraph above the v7.7.2 one.
TRG/SIG/AST recap (built Steps 1-7, behind the security-fix gate):
- TRG (scanners/trigger-scanner.mjs) — trigger/activation-abuse: TRG-shadow /
TRG-baiting / TRG-broad over name+description frontmatter, decode pipeline
first (LLM06/AST04).
- SIG (scanners/signature-scanner.mjs) — pure-Node known-malware identity engine
over raw bytes + decode pipeline; rules in knowledge/signatures.json
(LLM03/LLM02).
- AST (scanners/ast-taint-scanner.mjs) — parse-only python3 taint helper
(scanners/lib/py-ast-taint.py) with graceful regex fallback (LLM01/LLM02/AST02).
Release gate: full node --test suite 1863/0 after the doc edits. No scanner,
hook, or command behaviour changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
Session B of the security-fix track. Both sinks trusted untrusted strings at a
filesystem/subprocess boundary; same fix family as F-1.
F-2 (HIGH, arbitrary file write) — scanners/auto-cleaner.mjs: applyFixes() did
resolve(targetPath, f.file) with no containment, then wrote the cleaned content
back. f.file is untrusted (scanned-repo filenames, or a fully attacker-chosen
--findings file), so file: "../../.claude/settings.json" let the cleaner modify
files OUTSIDE the scanned tree. Add a prefix-containment check before grouping:
absPath must equal targetPath or start with targetPath + sep, else the finding
is refused and reported as skipped. (Documented residual gap: prefix containment
does not stop a symlink inside the tree pointing out — noted inline.)
F-3 (HIGH, command injection, pre-confirmation) — hooks/scripts/
pre-install-supply-chain.mjs: inspectNpmPackage ran execSafe(`npm view ${spec}
--json`), a shell string. spec derives from package tokens parsed out of the
scanned Bash command, so a metachar-bearing token reached the shell on
PreToolUse(Bash) — BEFORE the install, so it ran even if the user then denied
the command. Switch to spawnSync('npm', ['view', spec, '--json']) (no shell);
spec is passed as one argv element. The static `npm audit --json` execSafe call
is left as-is (no interpolation).
TDD (repro -> red -> green):
- tests/scanners/auto-cleaner-traversal.test.mjs: a "../secret.txt" finding must
not rewrite the outside file; contained files still get cleaned.
- tests/hooks/supply-chain-injection.test.mjs: `npm install $(>/abs/PWNED)`
(redirect-only $(...) survives normalizeBashExpansion + the whitespace split)
must not create the sentinel.
Closing gates: full node --test suite 1863/0 (was 1860; +3); gitleaks clean;
F-1 regression re-run GREEN (sink still closed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
git-forensics ran execSync(`git ${cmd}`), interpolating attacker-controlled
filenames from the SCANNED repo (git ls-files / git log --name-only) into a
shell string. A hostile repo containing a file named `commands/$(touch X).md`
achieved zero-interaction RCE on `/security scan <url>`: gitScan is in the
default scanner array and runs OUTSIDE the git-clone OS sandbox (which wraps
only the clone), so it executed unsandboxed on all platforms.
Convert the git() helper to spawnSync('git', [...args]) with no shell; every
call site now passes discrete tokens (shell quoting removed — git does its own
pathspec globbing). The helper throws on non-zero exit, preserving existing
per-category/per-file try/catch semantics.
TDD: adds a failing-first regression (tests/scanners/git-injection.test.mjs)
that builds a hostile-filename fixture repo and asserts the injected command
never runs. RED against execSync, GREEN after the fix.
Also removes two stray committed root artifacts (F-5/F-6): `--json`
(0-byte redirect husk) and .orphaned_at.
Closing gates: full node --test suite 1860/0; gitleaks clean. F-2/F-3 follow
in Session B.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
Action brief from the marketplace-wide review (see docs/review-2026-06-20.md). Three shell/path
injection sinks: F-1 CRITICAL zero-interaction RCE in git-forensics.mjs (execSync shell string +
attacker-controlled filenames; reproduced), F-2 HIGH path-containment write in auto-cleaner.mjs,
F-3 HIGH command injection in the supply-chain hook. Common fix: execSync(string) -> spawnSync(array)
/ input containment. DO AFTER the current active session finishes.
Disclosure hold: this brief + review-2026-06-20.md (ff9bd13) are committed locally but NOT pushed;
push both only together with the F-1 fix (the brief carries a working RCE repro payload).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
Grade B− — exceptionally disciplined design, but one CRITICAL zero-interaction RCE (F-1) plus two HIGH shell/path sinks. PUSH PARKED pending responsible-disclosure decision. Part of the marketplace-wide review (config-audit v5.4.0 + llm-security + structure + version). Read-only; this file is the only artifact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
Brief + 8-step trekplan for 3 new dep-free scanners: TRG (trigger-abuse),
SIG (signature engine over the decode pipeline), AST (Python AST taint).
Adversarial review fixed 3 blockers + 7 majors on draft 1 (59/D -> 86/B).
STATE.md prepped as a self-contained cold-start for /trekexecute next session.
Un-gitignore STATE.md (global ~/.claude rule overrides old polyrepo convention);
ignore generated plan annotation HTML. No scanner code yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
Tre fixes commited etter v7.7.0-tagen (adffb60 + 52afd12 + 02a66f3) viste
versjons-inkonsistens: package.json + plugin.json + README badge + CLAUDE.md
header satt fortsatt på v7.7.0 mens commit-meldinger og inline-kommentarer
refererte v7.7.1 som om det var en release. Per feedback_version_sync.md
skal alle versjonsreferanser stemme — denne commiten lukker gapet.
Endringer:
- package.json: 7.7.0 → 7.7.1
- .claude-plugin/plugin.json: 7.7.0 → 7.7.1
- plugin README badge: version-7.7.0-blue → version-7.7.1-blue
- plugin README "Recent versions"-tabell: ny [7.7.1]-rad
- plugin CLAUDE.md header + v7.7.1-highlights state-seksjon
- docs/version-history.md: ny v7.7.1-seksjon
- playground HTML linje 6935: 'Plugin v7.7.0' → 'Plugin v7.7.1'
(samme template-litteral som v7.7.0-bumpen ikke fanget, nå synket)
- CHANGELOG.md: ny [7.7.1]-seksjon med full Changed/Fixed/Notes
- rot README llm-security-entry: v7.7.0 → v7.7.1 + ny v7.7.1-bullet
- rot CLAUDE.md plugin-katalog: v7.7.1-bump
Verifisert:
- 1820/1820 tester grønne (pre-compact-flake fyrte ikke)
- CLI rapporterer fornuftig feilmelding på tom input
- Ingen kildefil-treff på 7.7.0 utenfor CHANGELOG/version-history/REMEMBER/TODO/ROADMAP
Ingen ny atferd. Kun versjons-synking + dokumentasjon av tre fixes som var
deployert som ad-hoc-commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Etter v7.7.1-strippen ble onboarding fjernet, men topbar viste fortsatt
demo-state-organisasjonsnavnet ('Direktoratet for digital tjeneste-
utvikling') hentet fra shared.organization. Det er ikke meningsfullt
lenger siden onboarding ikke kan endre verdien.
Erstattet med statisk 'llm-security' som nøytralt scope-anker. Crumb-
parameteren (f.eks. 'Katalog') beholdes som suffix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Operatør-tilbakemelding etter v7.7.0: hjem-overflaten ledet fortsatt med
prosjekter (Re-onboard / Nytt prosjekt / Command-katalog) — katalog var
tredje kort, sekundært bak prosjekt-tracks. Brukeren ba om å fjerne
onboarding + prosjekter og beholde katalog ('Vi legger til funksjonalitet
senere').
Minimum-strip (gammel kode bevart, kun routing + topbar endret):
- renderActive(): tvinger alltid activeSurface til 'catalog'.
Onboarding/home/project-render-funksjonene er bevart men ikke rutbare.
- Init-default endret fra 'home' til 'catalog' (også for migrerte states).
- Topbar: 'Hjem' og 'Re-onboard'-knappene fjernet. 'Katalog' beholdt
sammen med Eksporter/Importer/tema-toggle.
Konsekvens: playgrounden lander direkte i Command-katalog (20 kommandoer
med list-view + builder-pane + copy-knapp fra sesjon 1). Project-state +
onboarding-state forblir i IndexedDB men ingen UI-vei dit. Når funksjon-
alitet legges til igjen kan routeren utvides og topbar-knapper restaureres.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hjem-overflaten viste fortsatt 'Plugin v7.6.1' i meta-linja fordi
versjon-strengen er hardkodet inline i renderHome (line 6933), ikke
synkronisert med package.json. Fanget post-release.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>