Repository navigation
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:
gitlab:, which has been a built-in Bundler git source since 2.x (add_git_sourcesinbundler/dsl.rb).- Any custom
git_source(:name) { … }, for examplegit_source(:internal) { |r| "https://git.corp/#{r}.git" }withgem "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_optionshould fire, nothing should be rewritten and nothing attested. The code's own comment ongem_tail_source_optionsays 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-depsourceblock", which only works when the block actually decides the source. - Actual:
gitlab:and customgit_sourcekeys 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 onpath:+ git rather than staying silent.
Main 045d7ec, latest release tag v4.0.0. Not covered by the open #340 / #637 or #577 / #621.
Activity
bundler: run 11 ledger (#652)
mikolalysenko commentedon Oct 3, 2026
[agent] Triaged: priority:p1 (Bundler, false VEX attestation). Not a duplicate: #340 / PR #637 covers multi-line and modifier gem lines, and #577 / PR #621 covers global config. This issue is about the source-option token list in
gem_tail_source_option, which hosted (redirect/mod.rs) and vendored (vendor/gem.rs) both copy. A fix should go through one shared check, and ideally key on the lock'sGIT/PATHsections, so the two modes can't drift apart again.Generated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] Claiming this issue (shared root cause: the Gemfile source-option guard is a fixed token list shared by hosted and vendored modes, so it misses
gitlab:, customgit_sourcekeys and string-keyed"git" =>options). Branch: agent/fix-gem-source-option-guard. Claim-ID: 2026-10-03T23:20:55Z-ca7dddGenerated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] Draft PR: #731
Generated by Claude Code
Refuse gem lines pulled from git sources
Add e2e and escape tests for gem git sources
Read the gem lock's git sections once per scan