Skip to content

harden(supply-chain): newline-chain + Windows launcher + bare poetry/uv bypasses - #142

Merged
thehoff merged 1 commit into
developfrom
harden/supply-chain-bypass-followups
May 23, 2026
Merged

thehoff merged 1 commit into
developfrom
harden/supply-chain-bypass-followups

Conversation

@thehoff

@thehoff thehoff commented May 23, 2026

Copy link
Copy Markdown
Owner

Three tight follow-ups from the two-model peer review of #139 (Codex + Antigravity/Gemini). Each addresses a distinct bypass observed during review of the absolute/relative-path fix.

What lands here

1. Newline chain (Antigravity BLOCKER)

The arg-capture group `[^|;&<>]+` in all 7 install-detect regexes matched newlines, so a multi-line script chain-swallowed the next line and suppressed downstream detection. Adds `\r\n` to the negated class.

```

Before this PR: 1 detection (greedy first match swallows the rest)

After: 2 detections

echo PKG1
echo PKG2
```

2. Windows launcher (Codex MEDIUM)

`npm.cmd`, `pip.exe`, `yarn.cmd` slipped past both the package-bearing regexes (which required `npm\s+`) and the bare-install token match (`match tok { "npm" => … }`). Two-line fix:

  • All 7 regex verbs accept an optional `(?:\.(?:cmd|exe|bat))?` suffix.
  • `installer_basename` strips those suffixes after path-separator splitting, so `C:\Tools\npm.cmd` classifies as `npm` for the bare-install path.

3. Bare poetry / uv lockfile install (Antigravity HIGH)

`detect_bare_lockfile_installs` only covered npm/pnpm/yarn, so `poetry install` and `uv sync` (resolving the full dep tree from pyproject.toml / poetry.lock / uv.lock with no package args) returned a silent `Skip`. Extended to recognise both verbs and surface them as Pypi-ecosystem unvettable installs. Reshapes the internal `is_bare_install_verb: bool` thread into `Option` so the push records the right ecosystem (was hardcoded `Ecosystem::Npm`); unvettable lockfile-source string now forks per ecosystem.

Out of scope, tracked

Tests

12 new adversarial cases:

```
newline_chain_both_installs_detected // \n
newline_chain_carriage_return_also_caught // \r\n
windows_launcher_npm_cmd_detected
windows_launcher_pip_exe_detected
windows_launcher_abs_path_npm_cmd_detected // C:\Tools\node\npm.cmd …
windows_launcher_bare_npm_cmd_detected
poetry_install_bare_lockfile_detected
poetry_install_with_flags_still_bare_lockfile
poetry_add_still_handled_by_regex // sanity: package-bearing
uv_sync_bare_lockfile_detected
uv_pip_sync_bare_lockfile_detected
uv_abs_path_sync_detected
```

2,594 tests pass (+12 from develop), clippy clean on changed file.

Peer-review provenance

Findings consolidated from Codex (verdict: needs-work) and Antigravity/Gemini (verdict: needs-work). Both reviewers converged on the regression risk (#141) and the shell-quoting bypass class (#140). Antigravity flagged the newline-chain BLOCKER which Codex missed; Codex flagged the Windows launcher MEDIUM which Antigravity missed. Multi-model peer review is paying its keep.

🤖 Generated with Claude Code

…oetry/uv bypasses

Three tight follow-ups from the two-model peer review of #139
(Codex + Antigravity / Gemini). The shell-quote tokeniser refactor and
the plaintext-data false-positive guard are tracked in #140 and #141 —
both bigger work, deliberately not stuffed here.

1. NEWLINE CHAIN (Antigravity BLOCKER). The arg-capture group
   `[^|;&<>]+` in all 7 install-detect regexes matched newlines, so a
   multi-line script like `npm install x\npip install y` chain-swallowed
   the second line and suppressed downstream detection. Adds `\r\n` to
   the negated class.

2. WINDOWS LAUNCHER (Codex MEDIUM). `npm.cmd`, `pip.exe`, `yarn.cmd`
   slipped past both the package-bearing regexes and the bare-install
   token match. Two-line fix: regex verb now accepts an optional
   `(?:\.(?:cmd|exe|bat))?` suffix; `installer_basename` strips those
   same suffixes after path-separator splitting.

3. POETRY / UV BARE LOCKFILE INSTALL (Antigravity HIGH).
   `detect_bare_lockfile_installs` only covered npm/pnpm/yarn, so
   `poetry install` and `uv sync` (resolving the full dep tree from
   pyproject.toml / poetry.lock / uv.lock with no package args) returned
   a silent Skip. Extended the match block to recognise both verbs and
   surface them as Pypi-ecosystem unvettable installs. Reshapes the
   `is_bare_install_verb: bool` thread to carry `Ecosystem` directly so
   the push records the right ecosystem (was hardcoded `Ecosystem::Npm`).
   The unvettable lockfile-source string also forks per ecosystem.

12 new regression tests:
  - newline_chain_both_installs_detected (`\n`)
  - newline_chain_carriage_return_also_caught (`\r\n`)
  - windows_launcher_{npm_cmd,pip_exe,abs_path_npm_cmd,bare_npm_cmd}_detected
  - poetry_install_bare_lockfile_detected
  - poetry_install_with_flags_still_bare_lockfile
  - poetry_add_still_handled_by_regex (sanity: package-bearing path)
  - uv_sync_bare_lockfile_detected
  - uv_pip_sync_bare_lockfile_detected
  - uv_abs_path_sync_detected

Out of scope, tracked:
  #140 — shell-quote-aware tokeniser (closes the HIGH bypass class:
         `sh -c '<install>'`, `$(<install>)`, backticks, quoted heads,
         quoted operators in flag values).
  #141 — plaintext-data false-positive guard (closes the MEDIUM
         regression risk: `echo "/usr/bin/npm install foo"` is data,
         not an install — hit live during this very peer review when
         agy received a prompt that quoted the verb).

2,594 tests pass (+12 from develop), clippy clean on changed file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thehoff
thehoff merged commit b6dba93 into develop May 23, 2026
4 checks passed
@thehoff
thehoff deleted the harden/supply-chain-bypass-followups branch May 23, 2026 01:55
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
…t bypasses

Round-two peer review of thehoff#142 by Codex + agy converged on a BLOCKER and
three HIGHs. All four landed here. Both reviewers verdict on thehoff#142 was
"hold"; thehoff#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 thehoff#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 thehoff#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>
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