Skip to content

harden(supply-chain): close value-flag, case, versioned-pip, line-cont bypasses - #143

Merged
thehoff merged 4 commits into
developfrom
harden/value-flag-and-launcher-edge-cases
May 23, 2026
Merged

thehoff merged 4 commits into
developfrom
harden/value-flag-and-launcher-edge-cases

Conversation

@thehoff

@thehoff thehoff commented May 23, 2026

Copy link
Copy Markdown
Owner

Third hardening pass on the supply-chain gate, addressing the BLOCKER + three HIGHs that Codex and agy both surfaced when peer-reviewing #142. #142 merged on the strength of what it DID close (per [thehoff] direction); this PR carves the remainder.

What lands

1. BLOCKER — value-consuming flag bypass (both reviewers AGREED)

poetry install --with dev, uv sync --extra dev, uv sync --group dev: `--with` / `--extra` / `--group` consume the next token as their value, but the `has_package` scan in `detect_bare_lockfile_installs` treated that value as a positional package, flipped `has_package=true`, and silently dropped the install from detection.

Fix: thread `accepts_packages: bool` through the per-verb match. Verbs that never take a positional (`poetry install`, `uv sync`, `uv pip sync`, `npm ci`, `yarn install`, bare `yarn`) skip the package scan unconditionally and always emit as bare. Verbs that do take a positional (`npm install`, `pnpm install`, `pnpm i`) keep the scan — the named case is normally claimed upstream by NPM_RE / PNPM_RE; the scan is the fallback.

2. HIGH — Windows launcher case-insensitivity (both reviewers AGREED)

`NPM.CMD install foo`, `Npm.Cmd install foo`, `PIP.EXE install x` slipped past both the regex verb check and the bare-install token comparison.

Fix: `installer_basename` now strips `.cmd`/`.exe`/`.bat` case-insensitively in a loop, so chained extensions (`npm.cmd.exe`) collapse. The bare-install token match lowercases the basename before comparing. All 7 install-detect regexes use `(?i:…)` scoped flags on the verb token and the suffix group: `(?i:npm)(?:\.(?i:cmd|exe|bat))\s+(?i:i|install|add)`. The suffix group quantifier changed from `?` to `` so chained extensions match via the regex too.

3. HIGH — Versioned pip launchers (Codex)

`pip3.12.exe install x` stripped `.exe` → `pip3.12`, which didn't match `pip|pip3`. The PIP_RE verb is now `(?i:pip\d*(?:\.\d+)*)`, matching `pip`, `pip3`, `pip3.12`, `pip3.12.0`, etc., paired with the chained-suffix group.

4. HIGH — Line-continuation bypass (agy) — own #142 regression

`\r\n` in the arg-capture's negated class (added in #142 to stop newline chain-swallowing) truncated `npm install \ foo` at the trailing backslash, so packages on the continuation line went undetected. My fix in #142 was overcorrective.

Fix: a `LINE_CONT_RE` (`\\\r?\n\s*`) preprocess at the top of `detect_installs` collapses backslash-newline-indent sequences to a single space BEFORE detection runs. The newline guard from #142 is preserved for genuine multi-command newline separation — pinned by `line_continuation_does_not_merge_distinct_commands`.

Bonus

agy MEDIUM (double-extension `npm.cmd.exe`) is closed for free by the loop strip + `*` regex quantifier above.

Tests

18 new regression tests across the 4 classes:

```
poetry_install_with_{value_consuming,only,without_and_extras}flag_still_bare
uv_sync_with
{extra,group}value_still_bare
npm_ci_with_value_consuming_flag_still_bare
yarn_install_with_value_consuming_flag_still_bare
windows_launcher
{uppercase_npm_cmd,mixed_case,full_uppercase_no_suffix,pip_exe_uppercase,double_extension}detected
versioned_pip
{3_12,3_12_exe,abs_path}detected
line_continuation
{multiline,crlf,does_not_merge_distinct_commands}
```

2,612 tests pass (+18 from develop's 2,594), clippy clean on the changed file.

Per-the-rule notice

The new `peer-review-codex-agy-mandatory` memory rule (locked in this session) says: do not open the "Merge?" question on a security/hardening PR without first having run both reviews and surfaced consolidated findings. This PR will get a Codex + agy review pass before merge.

🤖 Generated with Claude Code

thehoff and others added 2 commits May 23, 2026 12:08
…t bypasses

Round-two peer review of #142 by Codex + agy converged on a BLOCKER and
three HIGHs. All four landed here. Both reviewers verdict on #142 was
"hold"; #142 merged on the strength of what it DID close (per Hoff
direction), with this branch carving the remainder.

1. BLOCKER (Codex + agy AGREED). Value-consuming flag bypass on the new
   bare-lockfile surface. `poetry install --with dev`, `uv sync --extra dev`,
   `uv sync --group dev` — `--with`/`--extra`/`--group` consume the next
   token, but the `has_package` scan treated that value as a package and
   silently dropped the install. Fix: thread `accepts_packages: bool`
   through the per-verb match. `poetry install`, `uv sync`, `uv pip sync`,
   `npm ci`, `yarn install`, and bare `yarn` never take a positional
   package — for them the package scan is skipped, always-bare. `npm
   install` / `pnpm install` / `pnpm i` keep the scan as before (they do
   take a positional, and the named-install case is normally claimed by
   NPM_RE / PNPM_RE upstream).

2. HIGH (Codex + agy AGREED). Windows launcher case sensitivity. `NPM.CMD
   install foo`, `Npm.Cmd install foo`, `PIP.EXE install x` slipped past
   both the regex verb check and the bare-install token compare. Fix:
   - `installer_basename` strips `.cmd`/`.exe`/`.bat` case-insensitively
     in a loop (so chained extensions `npm.cmd.exe` also collapse).
   - The bare-install token match lowercases the basename before
     comparing.
   - All 7 install-detect regexes use `(?i:…)` scoped flags on the verb
     and the suffix groups: `(?i:npm)(?:\.(?i:cmd|exe|bat))*\s+(?i:i|install|add)`.
   - The optional suffix group changed from `?` to `*` so the regex
     matches chained extensions (`npm.cmd.exe install foo`).

3. HIGH (Codex only). Versioned pip launchers. `pip3.12.exe install x`
   stripped `.exe` to `pip3.12`, which didn't match `pip|pip3`. The
   PIP_RE verb is now `(?i:pip\d*(?:\.\d+)*)`, matching `pip`, `pip3`,
   `pip3.12`, `pip3.12.0`, etc., paired with the chained-suffix group.

4. HIGH (agy only). Line-continuation bypass — my own #142 regression.
   `\r\n` in the arg-capture's negated class truncated `npm install \<nl>
   foo` at the trailing backslash, so packages on the continuation line
   went undetected. Fix: a `LINE_CONT_RE` (`\\\r?\n\s*`) preprocess at
   the top of `detect_installs` collapses backslash-newline-indent
   sequences to a single space BEFORE detection runs. The newline guard
   from #142 is preserved for genuine multi-command newline separation
   (test `line_continuation_does_not_merge_distinct_commands`).

Also folds in agy MEDIUM (double-extension `npm.cmd.exe`) since the
loop strip + `*` regex quantifier address it for free.

18 new regression tests:
  - poetry_install_with_{value_consuming,only,without_and_extras}_flag_still_bare
  - uv_sync_with_{extra,group}_value_still_bare
  - npm_ci_with_value_consuming_flag_still_bare
  - yarn_install_with_value_consuming_flag_still_bare
  - windows_launcher_{uppercase_npm_cmd,mixed_case,full_uppercase_no_suffix,pip_exe_uppercase,double_extension}_detected
  - versioned_pip_{3_12,3_12_exe,abs_path}_detected
  - line_continuation_{multiline,crlf}_install_caught
  - line_continuation_does_not_merge_distinct_commands

2,612 tests pass (+18 from develop's 2,594), clippy clean on the changed
file (the line 688 warning is the pre-existing `parse_package_args`
type_complexity, unchanged).

Per the new peer-review-mandatory rule in memory, this PR will be
reviewed by Codex + agy before the merge question is opened.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to agy peer review on #143 (BLOCKER). The earlier
`LINE_CONT_RE` (`\\\r?\n\s*`) only collapsed `\n` and `\r\n` line
endings, so a backslash + bare `\r` (legacy Mac convention, still
emitted by some tools and very-old scripts) survived as a real
continuation to the shell but bypassed the gate's normalisation.

Regex now: `\\(?:\r\n|\n|\r)\s*` — covers POSIX, Windows, and legacy
Mac line endings.

+1 regression test: `line_continuation_legacy_mac_cr_only_caught`.

Out of scope from agy's #143 review (tracked separately): bun ecosystem,
pdm/pipenv/conda/mamba ecosystems, PEP 508 extras stripping, inline `#`
comment handling, pip/uv value-consuming-flag values being parsed as
packages. Each is its own scope, not a #143 closure.

2,613 tests pass (+1), clippy clean.

Codex peer review on #143 is still in flight; this commit is a
narrow agy-finding fix, not the consolidated action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thehoff and others added 2 commits May 23, 2026 12:21
…boundary

Codex peer review on #143 found two issues:

LOW: the bare-yarn rule over-matched any dash-prefixed next-token, so
`yarn --version`, `yarn -v`, `yarn --help`, `yarn -h` (diagnostic
invocations that touch nothing) were escalated as bare lockfile
installs. Fixed by introducing `is_yarn_help_or_version_flag(tok)` and
short-circuiting the bare arm for the four canonical query flags.
`yarn --frozen-lockfile` (an actual install flag) still classifies as
bare — pinned by `yarn_with_other_flag_still_bare_install`.

MEDIUM: the gate's install-detection surface (npm/pnpm/yarn/pip/uv/
poetry/pipx) is narrower than "all dependency acquisition", and the
absence of a documented boundary could give a false sense of
completeness. Per Codex's own recommendation ("document … OR add
detectors"), the boundary is now codified as a doc-comment block on
`detect_installs`. Ecosystem expansion (bun, pdm, pipenv, conda,
mamba, brew, plus `pnpm update`, `poetry update`) is tracked in #144 —
this commit narrates the scope explicitly, not as a silent omission.

+4 regression tests:
  - yarn_version_flag_not_a_bare_install
  - yarn_help_flag_not_a_bare_install
  - yarn_short_version_not_a_bare_install
  - yarn_with_other_flag_still_bare_install (sanity)

2,617 tests pass (+4), clippy clean.

Consolidated review on #143 (Codex + agy):
  - BLOCKER (CR-only line continuation, agy) — closed in 6ebe953.
  - LOW (yarn over-match, Codex) — closed here.
  - MEDIUM (coverage gap, both) — scope boundary documented, expansion
    tracked in #144 + #145 (per Codex's documented-OR-detect option).
  - All other agy findings — tracked as scope-expansion or parse-quality
    in #144 and #145.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aviour

Codex re-review INFO finding on PR #143: the scope-boundary doc-comment
opened with "anything outside this list returns Skip" which was slightly
stronger than the implementation. The `yarn` arm at the `next.starts_with('-')`
branch classifies `yarn --frozen-lockfile` (and any other non-diagnostic
flag form) as a bare install — that case IS in scope (it's Yarn's
shorthand for `yarn install`), and a test (`yarn_with_other_flag_still_bare_install`)
already pins it. The comment now lists `yarn <install-flag>` explicitly
alongside `yarn install` / bare `yarn`, and clarifies that the
diagnostic-flag exclusion is what filters the out-of-scope variants.

Documentation-only — no runtime change.

2,617 tests pass, clippy clean. Codex + agy re-review verdict on #143
this round: MANDATE CLEAR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thehoff
thehoff merged commit 711c0b4 into develop May 23, 2026
4 checks passed
@thehoff
thehoff deleted the harden/value-flag-and-launcher-edge-cases branch May 23, 2026 02:39
thehoff added a commit that referenced this pull request May 23, 2026
Manual version bump capping the #141 plaintext-FP guard cluster
(#139, #140/#146, #141/#148, #142, #143). Headline: zero-execution
data-utility allowlist (echo/printf/cat/grep/jq/base64/…) so that
install-shaped substrings printed-as-data no longer trigger the
supply-chain gate, with hard exclusions for rg / awk / gawk / sed
which can spawn subprocesses (Codex + agy round-3 GREEN/GREEN).

46 merged PRs since v0.1.9. See GH release v0.1.10 for the full
PR list + notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
noogalabs pushed a commit to noogalabs/contextcrawler that referenced this pull request Jun 4, 2026
thehoff#141)

The supply-chain gate's install-detection regexes match install-shaped
substrings anywhere in a command, including inside the ARGUMENTS of
ordinary data-consuming utilities. Live FP from peer review:

    echo "/usr/bin/npm install foo"

is data printed to stdout, but the gate treated the inner substring as
a real install verb and blocked.

Re-implementation of thehoff#141 on top of develop @ c4f6e55, composing
cleanly with the now-merged thehoff#142/thehoff#143/thehoff#146 work (line-continuation
preprocess, quote-aware tokeniser, recursion + depth-cap synthetic
Unvettable). The original thehoff#141 implementation (commit 7a6935b on
feat/plaintext-fp-guard-141) accumulated too many conflicts with the
follow-on merges to rebase cleanly — this is a fresh re-write of the
same byte-masking contract.

Design — head-verb allowlist, byte-mask preserving offsets:

- DATA_CONSUMING_UTILITIES — echo, printf, cat, tac, grep, egrep,
  fgrep, rg, awk, gawk, sed, head, tail, tee, nl. Same list as the
  original; deliberately narrow, only utilities whose canonical role
  is to EMIT or SEARCH their argv as data.
- command_head_is_data_utility(segment) — basename-normalises the
  first non-operator token (handles /bin/echo, /usr/bin/grep) and
  matches against the allowlist.
- mask_data_utility_segments(cmd) — walks the shell_tokens stream,
  identifies segments whose head is a data utility, overwrites their
  bytes with ASCII spaces in a same-length working copy. Chain
  operators (&&, ||, ;, |) and unmasked segments are preserved
  verbatim, so the regex prefix-anchors and the claimed-span dedup
  bookkeeping continue to work unchanged.

Composition with post-thehoff#146 develop — masking layer placement:

- Wired into detect_installs_into() (NOT detect_installs()), so it
  applies at every recursion depth. Wrappers like
  sh -c 'echo "npm install foo"' extract the inner echo payload
  via extract_recursion_segments and recurse; the inner head is
  also a data utility and must be masked at the inner depth.
- The recursion sweep below the masking now runs against cmd_raw
  (the unmasked input), NOT the masked copy. Reason: a substitution
  body nested inside a data-utility segment still executes at
  runtime — echo "$(npm install foo)" does install the package —
  so the gate must still vet that body. Masking suppresses the
  surface-level regex pass for that segment ONLY; the recursion
  sweep is left intact.
- Composes cleanly with mask_quoted_operators() (also same-length)
  and the LINE_CONT_RE collapse upstream.

Segment-end calculation differs from the original implementation —
post-thehoff#146 shell_tokens strips quote characters from token payloads
(so the literal token for "foo" is foo, payload len 3, source span
5 bytes including the quotes). The original mask used
seg_end = off + tok.len() which would under-mask quoted tokens.
This re-write tracks segment end via the next operator's source
offset (or cmd.len() at end-of-stream), which is robust to the
quote-stripping payload contract.

Tests: 13 new tests covering the FP cases (echo/grep/printf/cat/rg
with install-shaped argv), positive guards (vanilla + abs-path npm
install MUST still detect), tricky chain semantics (echo "x" &&
npm install foo, echo … ; npm install …, echo foo | npm install bar,
cd foo && npm install bar), abs-path data-utility heads
(/bin/echo …), plus a direct unit test on
command_head_is_data_utility. 2,653 tests pass (was 2,640), zero
regressions.

Out of scope (deferred to principled-fix follow-up): token-position
constraint and quote-context exclusion. They are the principled
fixes — this remains a head-verb allowlist bandaid and the
DATA_CONSUMING_UTILITIES list should shrink (or retire) once those
ship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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