[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
Bundler takes its manifest from BUNDLE_GEMFILE: from the environment, or from the project's .bundle/config (bundle config set --local gemfile Gemfile.next, stored as BUNDLE_GEMFILE: "Gemfile.next"). That's the standard dual-boot / next-Rails layout (Gemfile + Gemfile.next + Gemfile.next.lock).
socket-patch never consults that setting (there's no BUNDLE_GEMFILE reference anywhere in crates/). It always picks gems.rb or Gemfile by filename (crates/socket-patch-core/src/patch/redirect/mod.rs:6051-6070, crates/socket-patch-core/src/setup/gem/mod.rs:101-116). It does already read the same .bundle/config for BUNDLE_PATH (crates/socket-patch-core/src/crawlers/ruby_crawler.rs:30-48).
The result:
- Hosted (
scan --mode hosted / get --mode hosted) rewrites Gemfile + Gemfile.lock, reports status: success, redirected: 1 with no warnings, and the in-run VEX attests not_affected (redirected). bundle install then resolves from Gemfile.next, still pointed at upstream, and the app loads the unpatched gem.
setup appends the plugin block to Gemfile. setup --check exits 0 ("configured"), but bundler never reads that file, so the plugin never registers. After bundle pristine + bundle install the gem stays unpatched with no warning.
(The post-install socket-patch vex does notice: not_applied, no document. So the false attestation is the in-run one, the same shape as #341.)
Repro (Bundler 4.0.17, Ruby 3.3.6, Linux)
Hermetic fixture: a mock upstream compact index + patch registry + patches API (a kept-alive copy of crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs), serving vuln-gem 1.0.0 with a free patch.
mkdir proj && cd proj
printf 'source "%s/upstream"\n\ngem "vuln-gem"\n' "$MOCK" > Gemfile
cp Gemfile Gemfile.next
bundle config set --local path vendor/bundle
bundle install && BUNDLE_GEMFILE=Gemfile.next bundle install # Gemfile.lock + Gemfile.next.lock
bundle config set --local gemfile Gemfile.next # .bundle/config: BUNDLE_GEMFILE: "Gemfile.next"
rm -rf vendor # no stale install
socket-patch scan --mode hosted --json --yes --vex vex.json --vex-product pkg:gem/[email protected] \
--api-url "$MOCK" --org test-org --api-token x
# -> status success, redirected 1, rewrittenFiles [Gemfile, Gemfile.lock], warnings []
# -> vex.json: not_affected / inline_mitigations_already_exist "Patched via Socket patch … (redirected)"
bundle install
bundle exec ruby -e 'require "vuln_gem"; puts VulnGem.status; puts Bundler.default_gemfile'
# -> VULNERABLE
# -> /…/proj/Gemfile.next
setup variant, on the same layout:
socket-patch setup --yes # + Gemfile, + .socket/bundler-plugin
socket-patch get "$UUID" --yes … # agent mode, applies once
socket-patch setup --check; echo $? # 0
bundle install # no "Installed plugin socket-patch" line: plugin never registers
bundle pristine vuln-gem && bundle install
bundle exec ruby -e 'require "vuln_gem"; puts VulnGem.status' # VULNERABLE
Expected vs actual
- Expected: the rewriter edits the manifest/lock pair bundler actually loads, which is the principle the code already states for
gems.rb vs Gemfile ("the gem rewriter picks the pair bundler reads", crates/socket-patch-cli/src/commands/scan/hosted.rs:83-85; docs/ecosystems.md: "edits gems.rb + gems.locked when present (bundler prefers them over Gemfile)"). Otherwise it should fail closed with a warning, like redirect_gem_gemfile_spellings_diverge, and never emit a not_affected attestation or a passing setup --check for a file bundler ignores.
- Actual: it silently wires
Gemfile, reports success, attests not_affected, and setup --check passes, while bundler installs and loads upstream bytes.
Matrix
| OS |
Bundler |
Hosted scan + in-run VEX |
setup + --check |
| Linux |
4.0.17 |
fail (reproduced twice, two fresh projects) |
fail |
| Linux |
2.x |
untested; BUNDLE_GEMFILE / config gemfile behave the same since Bundler 1.x |
untested |
| macOS / Windows |
any |
untested; the selection logic is OS-independent |
untested |
The env-var-only form (BUNDLE_GEMFILE=Gemfile.next bundle install in a CI job) has the same outcome. It's arguably harder for socket-patch to see, but the committed .bundle/config form is on disk in the project, where the crawler already reads BUNDLE_PATH.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:6051-6070: the (gemfile_name, lock_name) choice considers only gems.rb / Gemfile.
crates/socket-patch-cli/src/commands/scan/hosted.rs:81-88: the fixed list of gem manifest names handed to the rewriter.
crates/socket-patch-core/src/setup/gem/mod.rs:101-116: discover_bundler_project ignores BUNDLE_GEMFILE. plugins.rb.tmpl also hard-codes Gemfile.lock in digest_inputs.
- The vendored mode (
crates/socket-patch-core/src/vendor/gem.rs) most likely shares the gap. I didn't verify it this run.
Tested on main f6b7fb9 (v4.0.0).
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
Bundler takes its manifest from
BUNDLE_GEMFILE: from the environment, or from the project's.bundle/config(bundle config set --local gemfile Gemfile.next, stored asBUNDLE_GEMFILE: "Gemfile.next"). That's the standard dual-boot / next-Rails layout (Gemfile+Gemfile.next+Gemfile.next.lock).socket-patch never consults that setting (there's no
BUNDLE_GEMFILEreference anywhere incrates/). It always picksgems.rborGemfileby filename (crates/socket-patch-core/src/patch/redirect/mod.rs:6051-6070,crates/socket-patch-core/src/setup/gem/mod.rs:101-116). It does already read the same.bundle/configforBUNDLE_PATH(crates/socket-patch-core/src/crawlers/ruby_crawler.rs:30-48).The result:
scan --mode hosted/get --mode hosted) rewritesGemfile+Gemfile.lock, reportsstatus: success,redirected: 1with no warnings, and the in-run VEX attestsnot_affected (redirected).bundle installthen resolves fromGemfile.next, still pointed at upstream, and the app loads the unpatched gem.setupappends thepluginblock toGemfile.setup --checkexits 0 ("configured"), but bundler never reads that file, so the plugin never registers. Afterbundle pristine+bundle installthe gem stays unpatched with no warning.(The post-install
socket-patch vexdoes notice:not_applied, no document. So the false attestation is the in-run one, the same shape as #341.)Repro (Bundler 4.0.17, Ruby 3.3.6, Linux)
Hermetic fixture: a mock upstream compact index + patch registry + patches API (a kept-alive copy of
crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs), servingvuln-gem 1.0.0with a free patch.setupvariant, on the same layout:Expected vs actual
gems.rbvsGemfile("the gem rewriter picks the pair bundler reads",crates/socket-patch-cli/src/commands/scan/hosted.rs:83-85; docs/ecosystems.md: "editsgems.rb+gems.lockedwhen present (bundler prefers them overGemfile)"). Otherwise it should fail closed with a warning, likeredirect_gem_gemfile_spellings_diverge, and never emit anot_affectedattestation or a passingsetup --checkfor a file bundler ignores.Gemfile, reports success, attestsnot_affected, andsetup --checkpasses, while bundler installs and loads upstream bytes.Matrix
setup+--checkBUNDLE_GEMFILE/config gemfilebehave the same since Bundler 1.xThe env-var-only form (
BUNDLE_GEMFILE=Gemfile.next bundle installin a CI job) has the same outcome. It's arguably harder for socket-patch to see, but the committed.bundle/configform is on disk in the project, where the crawler already readsBUNDLE_PATH.Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:6051-6070: the(gemfile_name, lock_name)choice considers onlygems.rb/Gemfile.crates/socket-patch-cli/src/commands/scan/hosted.rs:81-88: the fixed list of gem manifest names handed to the rewriter.crates/socket-patch-core/src/setup/gem/mod.rs:101-116:discover_bundler_projectignoresBUNDLE_GEMFILE.plugins.rb.tmplalso hard-codesGemfile.lockindigest_inputs.crates/socket-patch-core/src/vendor/gem.rs) most likely shares the gap. I didn't verify it this run.Tested on main
f6b7fb9(v4.0.0).