[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
Bundler 1.x reads Gemfile before gems.rb. gems.rb only wins from Bundler 2.0 on (the prefer_gems_rb flag). When a project holds both spellings, hosted mode always follows the ≥ 2 order. On a project whose locks say BUNDLED WITH 1.17.3, scan --mode hosted:
- rewrites only
gems.rb/gems.locked;
- reports
status: success with only the usual redirect_gem_no_checksums_section / redirect_gem_frozen_install warnings;
- writes an in-run
--vex statement (not_affected).
Bundler 1.17.3 then installs from Gemfile/Gemfile.lock and gets the upstream (unpatched) gem. The post-install vex correctly refuses (exit 2), but the scan's own VEX document has already attested the CVE as mitigated.
crates/socket-patch-core/src/formats/gem/manifest.rs:18-20 notes that "1.x reads a Gemfile first, so callers treat a twin as ambiguous or follow the >= 2 order, as the hosted rewriter does". That trade-off isn't in docs/ecosystems.md or CLI_CONTRACT.md, and hosted mode neither warns nor refuses, even though both locks name the Bundler major (BUNDLED WITH 1.17.3). Vendored mode already refuses a twin outright (gemfile_not_loaded, after #341).
Impact
This produces a false OpenVEX not_affected and an unpatched runtime, with no warning, for Bundler 1.x projects that carry both spellings. It's a narrow shape, but it's exactly the "attestation for a patch that isn't applied" class.
Repro (hosted; patch API and registry mocked on loopback, real rubygems.org upstream)
gem install bundler -v 1.17.3 # Ruby <= 3.1 (1.17 calls String#untaint)
D='source "https://rubygems.org"\n\ngem "rake"\ngem "colorize", "~> 0.8.1"\n'
printf "$D" > Gemfile; printf "$D" > gems.rb
bundle _1.17.3_ config --local path vendor/bundle
BUNDLE_GEMFILE=Gemfile bundle _1.17.3_ lock; BUNDLE_GEMFILE=gems.rb bundle _1.17.3_ lock
socket-patch scan --mode hosted --json --yes --cwd . --vex out.vex.json --vex-product pkg:generic/app@1 ...
# status=success, rewrittenFiles=["gems.rb"], vex.statements=1
bundle _1.17.3_ install # loads Gemfile (Bundler.default_gemfile => Gemfile)
grep -c SOCKET_PATCHED vendor/bundle/ruby/*/gems/colorize-0.8.1/lib/colorize.rb # 0
socket-patch vex --output post.json ... # exit 2 (correctly refuses after install)
Expected vs actual
- Expected: docs/ecosystems.md says hosted mode "edits
gems.rb + gems.locked when present (bundler prefers them over Gemfile)", and the vendored column qualifies that with "which bundler ≥ 2 loads instead". When the locks show Bundler 1.x, the redirect should wire the Gemfile pair Bundler 1 actually loads, or refuse/warn the way a divergent or unsupported manifest does (redirect_gem_gemfile_spellings_diverge, redirect_gem_bundle_gemfile_unsupported). It shouldn't attest in the same run.
- Actual: it wires
gems.rb silently, the in-run VEX attests, and Bundler 1.17 installs the unpatched gem.
| OS |
Ruby |
Bundler |
Shape |
Bundler loads |
Scan |
In-run VEX |
Installed patched |
Post-install vex |
| ubuntu-latest |
3.1 |
1.17.3 |
twin |
Gemfile |
success, wires gems.rb |
1 statement |
no |
exit 2 |
| windows-latest |
2.7 |
1.17.3 |
twin |
Gemfile |
success, wires gems.rb |
1 statement |
no |
exit 2 |
| ubuntu-latest / windows-latest |
2.7 / 3.1 |
1.17.3 |
Gemfile only / gems.rb only |
the same file |
success |
1 |
yes |
0 |
| ubuntu-latest / windows-latest |
2.7 / 3.1 |
2.2.33 |
twin |
gems.rb |
success |
1 |
yes |
0 |
| ubuntu-latest / windows-latest |
2.7 / 3.1 |
2.2.33 |
Gemfile only / gems.rb only |
the same file |
success |
1 |
yes |
0 |
(The ubuntu-latest Ruby 2.7 job ran the 1.17.3 cells too. Bundler 1.17 can't run on Ruby ≥ 3.2.)
Suspect code
crates/socket-patch-core/src/formats/gem/manifest.rs:76 (LoadedManifest::pair, the Default if gems_rb_present => "gems.rb" arm) and the comment at lines 18-20.
crates/socket-patch-core/src/hosted/engine.rs (keep_bundler_loaded_gem_files), which applies that order without looking at BUNDLED WITH.
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
Bundler 1.x reads
Gemfilebeforegems.rb.gems.rbonly wins from Bundler 2.0 on (theprefer_gems_rbflag). When a project holds both spellings, hosted mode always follows the ≥ 2 order. On a project whose locks sayBUNDLED WITH 1.17.3,scan --mode hosted:gems.rb/gems.locked;status: successwith only the usualredirect_gem_no_checksums_section/redirect_gem_frozen_installwarnings;--vexstatement (not_affected).Bundler 1.17.3 then installs from
Gemfile/Gemfile.lockand gets the upstream (unpatched) gem. The post-installvexcorrectly refuses (exit 2), but the scan's own VEX document has already attested the CVE as mitigated.crates/socket-patch-core/src/formats/gem/manifest.rs:18-20notes that "1.x reads aGemfilefirst, so callers treat a twin as ambiguous or follow the >= 2 order, as the hosted rewriter does". That trade-off isn't in docs/ecosystems.md or CLI_CONTRACT.md, and hosted mode neither warns nor refuses, even though both locks name the Bundler major (BUNDLED WITH 1.17.3). Vendored mode already refuses a twin outright (gemfile_not_loaded, after #341).Impact
This produces a false OpenVEX
not_affectedand an unpatched runtime, with no warning, for Bundler 1.x projects that carry both spellings. It's a narrow shape, but it's exactly the "attestation for a patch that isn't applied" class.Repro (hosted; patch API and registry mocked on loopback, real rubygems.org upstream)
Expected vs actual
gems.rb+gems.lockedwhen present (bundler prefers them overGemfile)", and the vendored column qualifies that with "which bundler ≥ 2 loads instead". When the locks show Bundler 1.x, the redirect should wire theGemfilepair Bundler 1 actually loads, or refuse/warn the way a divergent or unsupported manifest does (redirect_gem_gemfile_spellings_diverge,redirect_gem_bundle_gemfile_unsupported). It shouldn't attest in the same run.gems.rbsilently, the in-run VEX attests, and Bundler 1.17 installs the unpatched gem.Matrix (probe run https://github.com/SocketDev/socket-patch/actions/runs/37176834794)
vexGemfilegems.rbGemfilegems.rbGemfileonly /gems.rbonlygems.rbGemfileonly /gems.rbonly(The ubuntu-latest Ruby 2.7 job ran the 1.17.3 cells too. Bundler 1.17 can't run on Ruby ≥ 3.2.)
Suspect code
crates/socket-patch-core/src/formats/gem/manifest.rs:76(LoadedManifest::pair, theDefault if gems_rb_present => "gems.rb"arm) and the comment at lines 18-20.crates/socket-patch-core/src/hosted/engine.rs(keep_bundler_loaded_gem_files), which applies that order without looking atBUNDLED WITH.