Skip to content

Hosted gem VEX attests not_affected for an unpatched install when .bundle/config sets an out-of-tree path (absolute or ~/…), because the skipped bundle root counts as "nothing installed" #709

Description

[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).

Summary

bundle config set --local path <dir> with <dir> outside the project (an absolute path such as /opt/bundle or /tmp/x, or ~/.bundle-store) is an ordinary Bundler setup. The ruby crawler deliberately refuses a config-sourced BUNDLE_PATH outside the project root, because that root is also an apply write target (resolve_config_bundle_path, containment guard). For hosted mode, though, that refusal means:

  1. The stale-install guard never probes the real install root, so a scan on a project whose unpatched gem is already installed there gives no redirect_gem_stale_install.
  2. The in-run scan --mode hosted --vex attests the redirected gem not_affected.
  3. The next bundle install prints Using colorize 0.8.1 and keeps the unpatched bytes, because they're already in the configured path (the case the stale guard exists for).
  4. A standalone, hash-verifying vex (default verify mode) also attests not_affected (redirected). It finds no installed copy (it never looked in the configured root) and falls into the "absence of any installed copy is excused / lockfile basis" branch. vex prints no warning at all. scan prints a run-level gem_bundle_config_path_ignored warning, but that warning only says the root is ignored "as an install root", not that the redirect and VEX are unverified.

The comment at crates/socket-patch-cli/src/commands/vex.rs:588 says ""Absent" must mean the crawler LOOKED". It handles --ecosystems scoping, but a bundle root the crawler refused to look in also counts as "absent".

Impact

A false not_affected VEX statement for code that is still vulnerable and loaded at runtime (bundle exec loads <path>/ruby/3.3.0/gems/colorize-0.8.1/lib). This holds both in the in-run attestation and in the post-install verified attestation that's meant to catch exactly this. An env-sourced BUNDLE_PATH pointing at the same directory is handled correctly (stale warning, and vex refuses with not_applied), so only the .bundle/config spelling is affected.

Repro (Linux, Ruby 3.3.6, real rubygems.org upstream, local mock of the patch API + patch registry)

The mock serves /v0/orgs/org/patches/{batch,by-package,package,view} for pkg:gem/colorize@0.8.1 and a compact index at /patch-registry/gem/tok/<uuid>/ serving a rebuilt colorize-0.8.1.gem whose lib/colorize.rb ends in # SOCKET_PATCHED. That's the same shape as the fixture in e2e_redirect_gem_build.rs.

OUT=/tmp/outside-bundle; rm -rf proj "$OUT"; mkdir proj && cd proj
printf 'source "https://rubygems.org"\n\ngem "colorize", "0.8.1"\n' > Gemfile
bundle config set --local path "$OUT"          # or: path '~/.bundle-store'
bundle install && bundle lock --add-checksums
A="--api-url http://127.0.0.1:18765 --org org --api-token fake"
socket-patch scan --mode hosted --json --yes --cwd . $A --vex out.vex.json --vex-product pkg:generic/app@1
#  redirect.redirected = 1, redirect.warnings = []   <- no redirect_gem_stale_install
#  out.vex.json: GHSA-… not_affected
bundle install                                     # "Using colorize 0.8.1", exit 0
grep -c SOCKET_PATCHED "$OUT"/ruby/3.3.0/gems/colorize-0.8.1/lib/colorize.rb   # 0 -> unpatched
socket-patch vex --output post.json --product pkg:generic/app@1 --cwd . --patch-server-url http://127.0.0.1:18765 $A
#  exit 0, "Wrote OpenVEX document with 1 statement", status not_affected,
#  impact_statement "Patched via Socket patch <uuid> (redirected)"

Expected vs actual

  • Expected (CLI_CONTRACT.md, "Gem stale-install guard"): a materialization bundle install would reuse is flagged redirect_gem_stale_install, and "a stale-flagged purl is additionally excluded from the same run's --vex assume_applied set — the envelope must never attest a CVE its own warning says is live". The post-install vex should refuse (not_applied), as it does for the same directory supplied through env BUNDLE_PATH. At the very least, when the crawler skipped a configured bundle root it shouldn't treat the gem as "not installed" for the lockfile-basis excuse, and vex should say so. The containment guard protects writes; reading the root to verify or to detect staleness doesn't write anything.
  • Actual: no stale warning; in-run and post-install VEX both attest not_affected while Bundler installs and loads the unpatched gem.

Matrix (Linux, Ruby 3.3.6; every cell ends in a real bundle install)

Bundle root spelling Bundler 4.0.17 Bundler 2.6.9 Bundler 2.4.22 (no CHECKSUMS)
config path absolute, outside the project fail (×2) fail fail
config path ~/.bh-bundle fail untested untested
config path relative vendor/bundle pass (stale warning; vex refuses not_applied) – –
config path absolute, inside the project pass – –
env BUNDLE_PATH absolute, outside the project pass – –

macOS / Windows: untested, but the containment check is lexical and OS-independent. On Windows, a drive-letter path to another folder (C:\bundle) would take the same branch.

First bad version: not bisected. The containment guard and the lockfile-basis VEX excuse both predate this ledger.

Suspect code

  • crates/socket-patch-core/src/crawlers/ruby_crawler.rs:341: the refused config root is recorded as skipped_config_path and never probed (resolve_config_bundle_path, :1092).
  • crates/socket-patch-cli/src/commands/vex.rs:581-600: the hosted lockfile-basis excuse applies to a gem whose bundle root was skipped, and vex never surfaces gem_bundle_config_path_ignored (only scan/mod.rs:1602 and apply.rs:1813 do).
  • The hosted stale-install probe (CLI_CONTRACT.md "Gem stale-install guard") reuses the same discovery, so it misses the root too.

Related: #686 is the Composer twin (absolute or ~ config.vendor-dir). #577 is a different missing layer (Bundler's global config).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions