Skip to content

Hosted gem redirect rewrites only the first of a gem's declarations, so a gem listed in two group blocks makes every bundle install fail with "You cannot specify the same gem twice" #548

Description

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

Summary

Bundler accepts a Gemfile that declares the same gem more than once with the same requirement. It only prints "Your Gemfile lists the gem … more than once". A common example is a gem listed in both group :development and group :test. scan --mode hosted / get --mode hosted rewrite only the first matching gem line into the per-dep source "<patch registry>" do gem "x", "<ver>" end block, and leave the other declaration as it was. The two declarations then have different requirements (= 1.0.0 vs >= 0). Bundler refuses to parse the Gemfile, so every later bundle install (frozen or not) and every bundle exec exits 4.

The scan exits 0 with redirected: 1, no warning, and an in-run --vex that attests not_affected.

Vendored mode already detects this shape and fails closed with gemfile_declaration_not_editable: gem "x" is declared more than once in the Gemfile (crates/socket-patch-core/src/vendor/gem.rs:1371). Hosted mode has no equivalent check.

Impact

A project that commits the hosted rewrite can no longer install at all: CI breaks, the deploy breaks, and the VEX document from the same run claims the CVE is not exploitable. This is a different root cause from #482. #482 appends a second declaration when the first one isn't visible (eval_gemfile / a loop). Here both declarations are plain lines that the rewriter can see, but it edits only one of them.

Repro

This uses a hold-open copy of crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs (the redirect_scanned_project fixture with a CHECKSUMS lock: real gem build, mock upstream + patch registry + API on loopback), then a real Bundler:

cat > Gemfile <<'EOF'
source "$UPSTREAM"

group :development do
  gem "vuln-gem"
end

group :test do
  gem "vuln-gem"
end
EOF
bundle config set --local path vendor/bundle
bundle lock                 # warns "lists the gem vuln-gem (>= 0) more than once", writes the lock
socket-patch scan --mode hosted --json --yes --api-url $API --org test-org --api-token fake \
  --vex out.vex.json --vex-product pkg:gem/app@1.0.0
# exit 0, redirect.redirected = 1, warnings = [], VEX: GHSA-redirect-gem-real not_affected
# fresh checkout (Gemfile, Gemfile.lock, .socket, .bundle):
bundle install              # exit 4

The Gemfile after the scan:

source "http://127.0.0.1:33109/upstream"

group :development do
source "http://127.0.0.1:33109/patch-registry/gem/<token>/<uuid>/" do
  gem "vuln-gem", "1.0.0"
end
end

group :test do
  gem "vuln-gem"
end

The output of bundle install:

[!] There was an error parsing `Gemfile`: You cannot specify the same gem twice with different version requirements.
You specified: vuln-gem (= 1.0.0) and vuln-gem (>= 0). Bundler cannot continue.
 #  from …/Gemfile:10
 >    gem "vuln-gem"

A top-level declaration plus one inside group :test do … end gives the same result (the top-level line is rewritten and the group line is left alone, so install exits 4).

Expected vs actual

  • Expected: the hosted rewrite must leave a Gemfile that Bundler installs. docs/ecosystems.md describes hosted gem wiring as a "per-dep source block", and CLI_CONTRACT.md requires that an envelope never attests a CVE that its own wiring doesn't fix. For a gem declared more than once, hosted mode should either rewrite every declaration consistently or fail closed before writing anything, as vendored mode does (gemfile_declaration_not_editable). In the fail-closed case it should also leave the purl out of the in-run --vex set.
  • Actual: only the first declaration is rewritten; the scan exits 0 with no warning; --vex attests not_affected; every install exits 4.

Matrix (Linux, Ruby 3.3.6)

Bundler Lock Two group blocks Top-level + group Vendored, same shape
4.0.17 CHECKSUMS fail (install exit 4, ×2) fail (install exit 4) pass (refused, nothing written)
2.6.9 CHECKSUMS (--add-checksums) fail (install exit 4) untested untested
2.4.22 no CHECKSUMS fail (install exit 4) untested untested

The bug isn't OS-specific: it comes from the Gemfile text rewriter, and macOS / Windows weren't probed. It also reproduces with the released v4.0.0 (npm @socketsecurity/socket-patch@4.0.0) and with the head of draft PR #532, so it isn't a recent regression.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:5007: if let Some(m) = gem_line_re.captures(gf) takes only the first match of the gem-line regex, so later declarations of the same name are never examined.
  • Compare with crates/socket-patch-core/src/vendor/gem.rs:1371 (if found.len() > 1 { return Err("… declared more than once …") }).

No probe branch was needed (the bug doesn't depend on the OS).

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

    Labels

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:bundlerBundler (RubyGems)priority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions