Skip to content

Hosted gem redirect treats a gitlab: or custom git_source gem as patched, so Bundler keeps loading the unpatched git checkout while VEX attests not_affected #652

Description

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

Summary

The hosted gem redirect refuses declarations that pick their own source (redirect_gem_source_option), because Bundler allows one source per gem and a git or path option inside the Socket source … do block overrides the block. The refusal is a fixed token list (path:, git:, github:, gist:, bitbucket:, source: and their :sym forms). That list misses:

  1. gitlab:, which has been a built-in Bundler git source since 2.x (add_git_sources in bundler/dsl.rb).
  2. Any custom git_source(:name) { … }, for example git_source(:internal) { |r| "https://git.corp/#{r}.git" } with gem "x", internal: "x". This is a common pattern for private git hosts.

In both cases scan --mode hosted --vex moves the line into the patch-registry block with the git option still attached, reports "redirected": 1, exits 0 and writes a not_affected VEX statement. Bundler still resolves the gem from the GIT source, so the app keeps running the vulnerable code.

A third, milder variant: with a string-keyed hash rocket, gem "x", "git" => "…", the refusal also doesn't fire. gem_line_trailing_options then treats "git" as a quoted version constraint and returns no options, so the git source (and any other string-keyed option, such as "require" => false) is silently dropped. Here the patched gem does get installed, but the user's declared source and options are rewritten without any warning, which is the same thing the refusal exists to prevent.

Impact

False attestation: the VEX says the vulnerability is mitigated by a Socket patch, but the patched bytes are never installed. The redirect envelope reports success, and only the usual no-CHECKSUMS caveats appear.

Repro

I used a temporary probe hook in crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs (redirect_scanned_project, reverted afterwards) to replace the fixture Gemfile. For the git-sourced arms it puts the pristine vuln-gem 1.0.0 source in a local git repo. Everything else is the stock capstone: the real gem build, a wiremock compact index plus patch API, and the real bundle install.

Gemfile (custom git source):

source "http://127.0.0.1:PORT/upstream"

git_source(:local) { |r| "/tmp/…/repos/#{r}" }

gem "vuln-gem", local: "vuln-gem"

Gemfile (built-in gitlab:, with https://gitlab.com/vuln-gem/vuln-gem.git mapped to the local repo through GIT_CONFIG_* url.insteadOf, so the lock records the real gitlab remote):

source "http://127.0.0.1:PORT/upstream"
gem "vuln-gem", gitlab: "vuln-gem"

Then:

bundle install          # lock: GIT remote … specs: vuln-gem (1.0.0)
socket-patch scan --mode hosted --json --yes --vex out.vex.json --vex-product pkg:gem/app@1.0.0 \
  --api-url <mock> --org test-org --api-token fake
bundle install
bundle exec ruby -e 'require "vuln_gem"; puts VulnGem.status; puts $LOADED_FEATURES.grep(/vuln_gem/)'

Actual result (custom arm; the gitlab arm is identical apart from the remote):

scan: exit 0, "redirected": 1, warnings: redirect_gem_no_checksums_section, redirect_gem_frozen_install
Gemfile after:
  source "http://127.0.0.1:…/patch-registry/gem/<token>/<uuid>/" do
    gem "vuln-gem", "1.0.0", local: "vuln-gem"
  end
out.vex.json: pkg:gem/vuln-gem@1.0.0 "status": "not_affected" ("Patched via Socket patch … (redirected)")
bundle exec: VULNERABLE
  …/vendor/bundle/ruby/3.3.0/bundler/gems/vuln-gem-e258a4451c09/lib/vuln_gem.rb

String-key arm (gem "vuln-gem", "git" => "/tmp/…/repos/vuln-gem"): the Gemfile becomes gem "vuln-gem", "1.0.0" inside the block (the git option is gone, with no warning), and bundle exec loads the patched registry gem.

Expected vs actual

  • Expected: the same refusal as github: / git:. redirect_gem_source_option should fire, nothing should be rewritten and nothing attested. The code's own comment on gem_tail_source_option says an option that selects a source "OVERRIDES the block and the redirect becomes a silent no-op". docs/ecosystems.md describes hosted gem as a "per-dep source block", which only works when the block actually decides the source.
  • Actual: gitlab: and custom git_source keys aren't recognized, so the run silently does nothing useful and attests success. String-keyed "git" => / "path" => aren't recognized either, and the option is dropped.

A sturdier approach than extending the token list would be to refuse whenever the lock lists the gem under a GIT or PATH section rather than GEM, because that's what Bundler actually resolves from.

OS × version

OS Ruby Bundler custom git_source gitlab: "git" =>
Linux 3.3.6 4.0.17 reproduces (×2) reproduces option dropped
Linux 3.3.6 2.5.22 reproduces untested untested
macOS / Windows — — not probed; the token matching is OS-independent

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:5187 (gem_tail_source_option: fixed token list, symbol-key spellings only)
  • crates/socket-patch-core/src/patch/redirect/mod.rs:5164 (gem_line_trailing_options: a quoted hash-rocket key ends the option scan)
  • The vendored backend has the same token list at crates/socket-patch-core/src/vendor/gem.rs:1544. I didn't verify vendored end to end; there, Bundler likely errors on path: + git rather than staying silent.

Main 045d7ec, latest release tag v4.0.0. Not covered by the open #340 / #637 or #577 / #621.

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