chore(llm-security): v7.8.2 — security patch release
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
This commit is contained in:
parent
566c281b56
commit
f4c65070ff
7 changed files with 146 additions and 6 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "llm-security",
|
||||
"description": "Security scanning, auditing, and threat modeling for Claude Code projects. Detects secrets, validates MCP servers, assesses security posture, and generates threat models aligned with OWASP LLM Top 10.",
|
||||
"version": "7.8.1"
|
||||
"version": "7.8.2"
|
||||
}
|
||||
|
|
|
|||
67
CHANGELOG.md
67
CHANGELOG.md
|
|
@ -6,6 +6,73 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [7.8.2] - 2026-07-18
|
||||
|
||||
Security patch. Fixes five defects from the v7.8.1 completion review, four of
|
||||
which caused a scanner or hook to fail silently — reporting success while the
|
||||
check it was named for did not run. No feature changes. 1901 tests, 0 fail.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **HIGH — bare root/home targets bypassed the rm block**
|
||||
(`hooks/scripts/pre-bash-destructive.mjs`). The BLOCK rule's target
|
||||
alternation ended in a shared `\b`, and a word boundary cannot hold after `/`
|
||||
or `~` at end-of-command. `rm -rf /`, `rm -rf ~`, `rm -rf /*`, `rm -fr /` and
|
||||
the sudo-prefixed forms all fell through to WARN (exit 0) — advisory only,
|
||||
command executed. Only targets starting with a word character (`/etc`,
|
||||
`/usr`) were ever blocked. `\b` now applies to the `$HOME` alternative alone,
|
||||
where it is meaningful.
|
||||
|
||||
- **HIGH — entropy suppression keyed off the absolute path**
|
||||
(`scanners/entropy-scanner.mjs`). The test/fixture suppression rule matched
|
||||
`/(test|spec|fixture|mock|__test__|__spec__)/i` against the **absolute**
|
||||
path, so any directory name above the scan root silenced every entropy
|
||||
finding in the entire target while still reporting status `ok`. The rule now
|
||||
keys off the path relative to the scan root.
|
||||
|
||||
- **HIGH — one unparseable JetBrains plugin crashed the whole ide-scan**
|
||||
(`scanners/ide-extension-scanner.mjs`). The two manifest parsers disagree on
|
||||
how they signal failure: `parseVSCodeExtension` returns bare `null`,
|
||||
`parseIntelliJPlugin` returns a truthy `{ manifest: null, warnings }`. Only
|
||||
the bare-null form was guarded, so every JetBrains failure path dereferenced
|
||||
`manifest.hasSignature`; the TypeError escaped through `mapConcurrent`'s
|
||||
unguarded `Promise.all` and aborted the scan of every other installed
|
||||
extension. Guard widened, plus per-extension fault isolation at the call
|
||||
site.
|
||||
|
||||
- **HIGH — obfuscated injections were reported but not stripped**
|
||||
(`scanners/content-extractor.mjs`). `stripInjection` scanned both the raw and
|
||||
the decoded text but removed matches only via a literal replace against the
|
||||
raw text. For a decoded-only match, `match[0]` is the decoded string, which
|
||||
by construction does not occur in the raw text — so the replace was a silent
|
||||
no-op and the encoded payload reached the LLM agent verbatim via
|
||||
`sanitized_content`, alongside a finding announcing it. Every obfuscation the
|
||||
normalizer exists to defeat was affected. Decoded-only matches are now
|
||||
removed by redacting the source line; multi-line payloads that cannot be
|
||||
attributed are flagged `unstripped: true` rather than left silently.
|
||||
|
||||
- **Out-of-range numeric character reference emptied a plugin.xml field**
|
||||
(`scanners/lib/ide-extension-parser.mjs`). `decodeEntities` guarded
|
||||
`parseInt` with `Number.isFinite`, which bounds nothing, so any code point
|
||||
above `0x10FFFF` made `String.fromCodePoint` raise `RangeError`. The
|
||||
no-throw contract held (the per-field `safe()` wrapper catches it), but the
|
||||
affected field was discarded and replaced with `''`. Undecodable references
|
||||
are now left literal. Filed as HIGH; it is lower — such a document is not
|
||||
well-formed XML, so the plugin would not load in IntelliJ either.
|
||||
|
||||
### Changed
|
||||
|
||||
- `stripInjection` (content-extractor) and `scanOneExtension` (ide-extension-
|
||||
scanner) are exported for testing; `content-extractor.mjs` runs `main()`
|
||||
behind the standard `isMain` guard so importing it no longer executes the
|
||||
CLI. The remote-scan injection boundary had no direct test coverage before
|
||||
this release.
|
||||
|
||||
### Removed
|
||||
|
||||
- `tests/hooks/probe-rm.mjs` — a self-labelled temporary debug probe with no
|
||||
assertions, pointing at a hardcoded path into the installed marketplace copy.
|
||||
|
||||
## [7.8.1] - 2026-07-18
|
||||
|
||||
Security patch. Fixes a CRITICAL command-injection defect in the auto-cleaner
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# LLM Security Plugin (v7.8.1)
|
||||
# LLM Security Plugin (v7.8.2)
|
||||
|
||||
Security scanning, auditing, and threat modeling for Claude Code projects. 5 frameworks: OWASP LLM Top 10, Agentic AI Top 10 (ASI), Skills Top 10 (AST), MCP Top 10, AI Agent Traps (DeepMind). 1820+ unit, integration, and end-to-end tests (`tests/e2e/` covers the multi-hook attack chain, multi-session state simulation, and the full scan-orchestrator pipeline); mutation-testing coverage not published.
|
||||
|
||||
Release notes for v7.0.0 → v7.8.1: see `docs/version-history.md` — read on demand.
|
||||
Release notes for v7.0.0 → v7.8.2: see `docs/version-history.md` — read on demand.
|
||||
|
||||
**v7.8.2 highlights** — Security patch, no feature changes. Five defects from the v7.8.1 completion review, four sharing one failure mode: **the check reported success without running**. (1) `hooks/scripts/pre-bash-destructive.mjs` did not block `rm -rf /` or `rm -rf ~` — the target alternation `(?:\/|~|\$HOME)\b` ended in a word boundary that cannot hold after `/` or `~` at end-of-command, so the bare forms the rule is named for fell through to WARN (exit 0, command executed) while `/etc` and `$HOME` blocked normally, making the rule look functional from either end. (2) `scanners/entropy-scanner.mjs` matched its test/fixture suppression against the **absolute** path, so any ancestor directory named `test`/`spec`/`fixture`/`mock` silenced every entropy finding in the target while still returning status `ok`; it now keys off the relative path. (3) `scanners/ide-extension-scanner.mjs` guarded only `parseVSCodeExtension`'s bare-`null` failure signal, not `parseIntelliJPlugin`'s truthy `{ manifest: null, warnings }`, so any malformed JetBrains plugin dereferenced `manifest.hasSignature`; the TypeError escaped `mapConcurrent`'s unguarded `Promise.all` and aborted the scan of every other installed extension. Guard widened + per-extension fault isolation. (4) `scanners/content-extractor.mjs` — the remote-scan injection boundary — detected obfuscated injections but did not strip them: a decoded-only `match[0]` never occurs in the raw text, so the literal replace was a silent no-op and the payload reached the agent verbatim via `sanitized_content` alongside a finding announcing it. Removal is now line-level; unattributable multi-line payloads carry `unstripped: true`. This boundary had no direct test coverage before v7.8.2. (5) `scanners/lib/ide-extension-parser.mjs` emptied any plugin.xml field holding a character reference above `0x10FFFF` (`Number.isFinite` bounds nothing) — filed as HIGH, actually lower, since such a document is not well-formed XML.
|
||||
|
||||
**v7.8.1 highlights** — Security patch, no feature changes. Fixes a CRITICAL command injection in `scanners/auto-cleaner.mjs`: `validateContent()` syntax-checked candidate `.mjs`/`.js`/`.cjs` content via ``execSync(`node --check "${tmpPath}"`)``, where `tmpPath` derives from the **untrusted scanned-repo filename**. The v7.8.0 F-2 guard checks path containment but neither strips nor quotes shell metacharacters, so a file named ``x";<command>;".mjs`` closes the interpolated quote and injects a command; since `/security clean` runs live by default, scanning a hostile repository sufficed for arbitrary local command execution (live-PoC verified). Both subprocess sites — the syntax check and the CLI's inline scan-orchestrator fallback — now use `spawnSync` with an argv array, so no shell parses a path. Defense-in-depth: `applyFixes()` refuses findings whose `file` carries shell/control metacharacters, surfaced as `skipped`. `validateContent` is now exported so regression tests can drive the sink directly — the guard would otherwise mask a re-introduced shell.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
@ -628,6 +628,7 @@ demonstrations — each with `README.md`, fixture, run script, and
|
|||
|
||||
| Version | Date | Highlights |
|
||||
|---------|------|------------|
|
||||
| **7.8.2** | 2026-07-18 | **Silent-failure fixes from the completion review (HIGH).** Five defects, four sharing one failure mode: the check reported success without running. `hooks/scripts/pre-bash-destructive.mjs` did not block `rm -rf /` or `rm -rf ~` — the target alternation ended in a `\b` that cannot hold after a non-word character, so the bare forms the rule is named for fell through to WARN (exit 0, command executed) while `/etc` and `$HOME` blocked normally. `scanners/entropy-scanner.mjs` matched its test/fixture suppression against the **absolute** path, so any ancestor directory named `test`/`spec`/`fixture`/`mock` silenced every finding in the target and still returned status `ok`. `scanners/ide-extension-scanner.mjs` guarded only `parseVSCodeExtension`'s bare-`null` failure signal, not `parseIntelliJPlugin`'s truthy `{ manifest: null }`, so any malformed JetBrains plugin threw a TypeError that escaped `mapConcurrent`'s unguarded `Promise.all` and aborted the scan of every other extension. `scanners/content-extractor.mjs` detected obfuscated injections but did not remove them: a decoded-only `match[0]` never occurs in the raw text, so the literal replace was a no-op and the payload reached the LLM agent verbatim through `sanitized_content` — removal is now line-level, with unattributable multi-line payloads flagged `unstripped`. `scanners/lib/ide-extension-parser.mjs` emptied any plugin.xml field containing a character reference above `0x10FFFF`. No feature changes. 1901 tests, 0 fail. |
|
||||
| **7.8.1** | 2026-07-18 | **Auto-cleaner command-injection fix (CRITICAL).** `scanners/auto-cleaner.mjs` syntax-checked candidate `.mjs`/`.js`/`.cjs` content via ``execSync(`node --check "${tmpPath}"`)``, where `tmpPath` derives from the untrusted scanned-repo **filename**. The v7.8.0 F-2 guard checks path containment but does not strip or quote shell metacharacters, so a file named ``x";<command>;".mjs`` closes the interpolated quote and injects a command — and `/security clean` runs live by default, making a hostile repository sufficient for arbitrary local command execution. Reproduced with a live PoC before the fix. Both subprocess sites (syntax check + the CLI scan-orchestrator fallback) now use `spawnSync` with an argv array, so no shell parses a path. Defense-in-depth: `applyFixes()` refuses findings whose `file` carries shell/control metacharacters, reported as `skipped`. Regression coverage is split across both layers so the guard cannot mask a re-introduced shell in the sink. No feature changes. 1865 tests, 0 fail. |
|
||||
| **7.8.0** | 2026-06-20 | **TRG/SIG/AST deep-scan scanners.** Three new deterministic deep-scan scanners targeting the skills/agents attack surface, each with its own finding prefix, OWASP/AST mapping, policy block, and graceful-skip behaviour. **TRG** (`scanners/trigger-scanner.mjs`) inspects command/agent/skill `name` + `description` frontmatter for activation-surface abuse: `TRG-shadow` (name collides with a built-in and intercepts it), `TRG-baiting` (maximally-activating phrases that bait indiscriminate invocation), `TRG-broad` (generic name + universal-applicability claim); descriptions pass the decode pipeline first so obfuscated baiting still trips (LLM06/AST04). **SIG** (`scanners/signature-scanner.mjs`) is a pure-Node known-malware *identity* engine (webshells, reverse shells, cryptominers, hacktools) that tests each signature against both raw bytes and the decode pipeline (base64/hex/url/entity/unicode, homoglyph fold, rot13), so obfuscated known-malware a byte-matcher misses is still caught; rules ship in `knowledge/signatures.json` (LLM03/LLM02). **AST** (`scanners/ast-taint-scanner.mjs`) shells out to a PARSE-ONLY `python3` helper (`scanners/lib/py-ast-taint.py`) for variable-level, scope-aware Python taint analysis — higher recall than the ~70% regex `taint-tracer.mjs` — and falls back to the regex tracer when `python3` is unavailable, so the scan never hard-fails; the helper only `ast.parse`s the target and never executes it (LLM01/LLM02/AST02). Built behind a security-fix gate (F-1/F-2/F-3 shell-injection + path-traversal fixes landed first). 1863 tests, 0 fail. |
|
||||
| **7.7.2** | 2026-05-19 | **Language consistency pass.** Norwegian had crept into surface text across v7.5-v7.7. Per the `~/.claude/CLAUDE.md` convention (English for code and documentation, Norwegian for dialog only), this release translates: the HTML Report-step appended by all 18 skill commands, the canonical CLI renderer `scripts/lib/report-renderers.mjs` (display strings + JS comments), the playground UI strings, the `skill-scanner-agent` and `mcp-scanner-agent` system prompts, the Recent versions table and playground architecture prose in this README, the v7.7.x highlights in `CLAUDE.md`, and the llm-security entries in the marketplace root `README.md` + `CLAUDE.md`, plus six table cells in `docs/scanner-reference.md`. Demo-state fixture content for the `dft-komplett-demo` project (intentional Norwegian persona) and regex alternations that match Norwegian-language report markdown (`/^high\|^høy/`, `/resolution\|løsning/`) were preserved. No scanner, hook, or behavior changes — purely surface text. |
|
||||
|
|
|
|||
|
|
@ -2,6 +2,73 @@
|
|||
|
||||
Per-release notes for v7.0.0 onward. Imported from `CLAUDE.md` via `@docs/version-history.md`.
|
||||
|
||||
## v7.8.2 — Silent-failure fixes from the completion review (HIGH)
|
||||
|
||||
Security patch covering five defects found in the v7.8.1 completion review. No
|
||||
feature changes; full suite green at 1901/0 (1865 + 36 new regression tests).
|
||||
|
||||
The common thread in four of the five is **silent failure**: the scanner or
|
||||
hook reported success while the check it is named for did not run. That is the
|
||||
worst failure mode for a security tool, because a clean report is
|
||||
indistinguishable from a clean target.
|
||||
|
||||
- **`hooks/scripts/pre-bash-destructive.mjs`** — the rule named "Filesystem
|
||||
root destruction (rm -rf /)" did not block `rm -rf /`. Its target
|
||||
alternation `(?:\/|~|\$HOME)\b` ended in a word-boundary assertion, which
|
||||
cannot hold after `/` or `~` at end-of-command. Measured: `rm -rf /`,
|
||||
`rm -rf ~`, `rm -rf /*`, `rm -fr /` and `sudo rm -rf /` all fell through to
|
||||
WARN — advisory only, exit 0, command executed. `rm -rf $HOME` and
|
||||
`rm -rf /usr` were blocked, which is why the gap survived: the rule looked
|
||||
functional from either end. The `\b` now sits on the `$HOME` alternative
|
||||
alone, so `$HOMEDIR` is still not swallowed. The prior test file had encoded
|
||||
the defect as expected behaviour, with a NOTE attributing it to merged `-rf`
|
||||
flags — a misdiagnosis, since `rm -rf /etc` blocks fine with merged flags.
|
||||
|
||||
- **`scanners/entropy-scanner.mjs`** — the "test fixtures intentionally contain
|
||||
example secrets" suppression matched against the **absolute** path. Any
|
||||
ancestor directory name containing `test`, `spec`, `fixture` or `mock`
|
||||
therefore suppressed every entropy finding in the whole target: a repository
|
||||
cloned into a CI workspace, or checked out beneath a folder named `testing`,
|
||||
reported zero secrets with status `ok`. The rule now keys off the path
|
||||
relative to the scan root, which was already computed and passed alongside
|
||||
it. Genuine fixture suppression (a `*.test.mjs` file, a relative `tests/`
|
||||
directory) is unchanged.
|
||||
|
||||
- **`scanners/ide-extension-scanner.mjs`** — `parseVSCodeExtension` signals
|
||||
failure as bare `null`; `parseIntelliJPlugin` signals it as a **truthy**
|
||||
`{ manifest: null, warnings }`. Only the former was guarded, so all five
|
||||
JetBrains failure paths (no `lib/`, `lib/` not a directory, `lib/`
|
||||
unreadable, no jars, no extractable jar) reached `manifest.hasSignature` and
|
||||
threw. `mapConcurrent` awaited without a per-item catch, so `Promise.all`
|
||||
rejected and the entire ide-scan aborted — one malformed plugin directory
|
||||
took down the scan of every other installed extension, including, in an
|
||||
audit context, a plugin that is malformed precisely because it is hostile.
|
||||
|
||||
- **`scanners/content-extractor.mjs`** — this is the remote-scan indirection
|
||||
layer: agents are supposed to see a sanitized evidence package, never raw
|
||||
hostile content. `stripInjection` scanned the raw text *and* its decoded
|
||||
form, but removed matches with `sanitized.replace(match[0], …)` against the
|
||||
raw text only. A decoded-only `match[0]` does not occur in the raw text by
|
||||
construction, so the replace silently did nothing: the payload was handed to
|
||||
the agent verbatim through `sanitized_content` while the report announced a
|
||||
critical injection. Removal now redacts the offending source line, since
|
||||
decoding is not length-preserving and decoded offsets cannot be mapped back.
|
||||
A payload split across several lines still cannot be attributed; those
|
||||
findings carry `unstripped: true` instead of failing quietly. Whole-file
|
||||
redaction was rejected — `normalizeForScan` base64-decodes any long blob, so
|
||||
a benign asset could blank a file's entire evidence.
|
||||
|
||||
- **`scanners/lib/ide-extension-parser.mjs`** — `decodeEntities` guarded
|
||||
`parseInt` with `Number.isFinite`, which bounds nothing, so a character
|
||||
reference above `0x10FFFF` raised `RangeError` in `String.fromCodePoint`.
|
||||
Contrary to how this was filed, the documented no-throw contract held: the
|
||||
per-field `safe()` wrapper catches it. The real effect was quieter — the
|
||||
field was replaced with `''`, so a `<name>` carrying one such reference
|
||||
parsed as an empty name and name-based checks (JetBrains typosquat
|
||||
detection) ran against nothing. Severity is below HIGH: a document with a
|
||||
code point above `0x10FFFF` is not well-formed XML, so IntelliJ would reject
|
||||
the plugin as well. Undecodable references are now left literal.
|
||||
|
||||
## v7.8.1 — Auto-cleaner command-injection fix (CRITICAL)
|
||||
|
||||
Security patch for a CRITICAL defect introduced with the v7.8.0 auto-cleaner
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "llm-security",
|
||||
"version": "7.8.1",
|
||||
"version": "7.8.2",
|
||||
"description": "Security scanning, auditing, and threat modeling for Claude Code projects",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ after(async () => {
|
|||
});
|
||||
|
||||
async function makePluginRoot(name) {
|
||||
const root = await mkdtemp(join(tmpdir(), 'llmsec-jb-plugin-'));
|
||||
// Prefix must NOT start with `llmsec-jb-`: jetbrains-parser.test.mjs asserts
|
||||
// that no `llmsec-jb-*` directory survives anywhere in tmpdir, and that
|
||||
// assertion is global, so a shared prefix collides depending on test order.
|
||||
const root = await mkdtemp(join(tmpdir(), 'llmsec-nullmanifest-'));
|
||||
created.push(root);
|
||||
const dir = join(root, name);
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue