February 15, 2021·6 min read·engineeringrubybundlersecuritydependencies

Bundler 2.2.10 carries source priority through dependency resolution

Bundler 2.2.10 changes source selection for dependencies of gems declared in source blocks. The patch also separates source sections in lockfiles so package provenance survives resolution and recording.

A private gem name does not reserve that name on a public repository. If a dependency manager treats packages with the same name as interchangeable across sources, someone who controls a public name can influence an installation intended to use private software. Bundler's February 15 announcement describes a source-priority change in version 2.2.10 after renewed attention to dependency confusion.

The release addresses a specific relationship. A gem declared inside a source block has an intended repository, and its dependencies need source-selection behavior consistent with that declaration. Picking a higher version from a public source can satisfy a version constraint while violating that provenance expectation. The patch makes source priority participate in resolving indirect dependencies.

I like Ruby's readable syntax, including the way a Gemfile can express dependency intent. That readability is useful only if the resolver gives the declaration an equally clear meaning. Repository selection is part of the dependency contract, especially when public and private repositories use overlapping names.

A source block expresses more than a search hint

The announcement describes a private gem that depends on another private gem. Its Gemfile has a public default source and a block naming the private source. The intended behavior is to obtain both private packages from that block's repository even when the public repository contains a higher version with the indirect dependency's name.

This illustrative Gemfile expresses the same arrangement. The private URL and gem names are placeholders, so the example describes configuration rather than a runnable installation against a real private service.

source "https://rubygems.org"

source "https://gems.example.com" do
  gem "internal-client"
end

The gem specification for internal-client can name another gem as a dependency. That declaration describes a package requirement without independently proving which repository owns the intended implementation. The resolver has to combine the dependency graph with the source context established in the Gemfile.

This is why the issue cannot be reduced to choosing a sufficiently distinctive private package name. Naming can reduce accidental collisions, but it does not define a source-selection rule. A dependency manager needs a policy for what happens when a collision exists. The same question arises whether the public duplicate is malicious or merely unrelated software.

Version ordering cannot establish package identity

The 2.2.10 changelog identifies source priority for transitive dependencies as the security fix. It also names splitting RubyGems source sections in the lockfile. Both changes concern information that a package name and version cannot express on their own.

A version requirement constrains acceptable versions of the intended package. If candidates from different authorities are combined prematurely, a newer unrelated package can look preferable to the intended one. Version comparison has then answered a question that should have been settled only after choosing the relevant source.

The security consequence follows from that ordering. A public publisher can choose a version number under a name they control. That number does not give them authority over an identically named private package. Letting the highest number determine the source effectively transfers a repository decision to whoever can publish a competing candidate.

The new behavior attaches source context to dependency resolution instead of treating all available specifications as one undifferentiated set. This keeps repository policy relevant when the resolver considers an indirect dependency. The developer's direct declaration then informs more than the first edge in the graph.

The resolver chooses a relevant index

The implementation patch changes how the resolver obtains candidate specifications. An explicit source requirement supplies that source's specifications. Otherwise, the separate-source path considers relevant sources associated with the dependency. The final source-priority change searches those sources in order for candidates rather than aggregating them into one index.

That mechanism matters because an aggregate index can erase priority before version selection occurs. Once all candidates sit together, a higher version from another source can win. Keeping source choice earlier in candidate lookup gives the declared preference a place to affect the result.

The regression scenario makes this visible with deliberately different package implementations. One repository contains the intended dependency. Another contains a higher version whose code signals an incorrect selection. The expectation checks both the selected version and its source. A test that asserted only that some package with the right name installed would miss the defect.

This is a useful distinction for resolver testing. The final dependency graph can satisfy every version constraint and still be wrong about provenance. Correctness assertions need to include where the chosen specifications originated. Otherwise, a source-selection regression can remain invisible behind an apparently successful resolution.

A lockfile must retain source distinctions

The patch also changes how separate repositories appear in lockfiles. A single GEM section with several remote entries does not preserve the same clear package-to-source association as individual source sections. Recording the result needs to retain the provenance decisions made during resolution.

The source regression specifications exercise conversion from an aggregate source section to separate sections. Their expected lockfile assigns the selected package to its specific remote. This makes source identity part of the recorded result rather than merely an ephemeral input to the first installation.

That relationship is relevant when another environment installs from a lockfile. Reproducing version numbers does not by itself reproduce the source choice. The lockfile needs enough information to distinguish repositories that can serve the same name. Otherwise, the apparent reproducibility of a version list can conceal ambiguity about the actual package being obtained.

Frozen and deployment workflows introduce a compatibility constraint because they are not supposed to rewrite the lockfile casually. The patch's tests preserve the aggregate lockfile in that case and exercise a warning for the existing Bundler line. The diagnostic asks users to regenerate the insecure combined-source representation in a non-frozen environment. This is a migration behavior with consequences for how source information reaches deployment.

Source policy and package review answer different questions

The announcement discusses RubyGems.org's handling of the research packages that brought attention to the issue. Repository-side detection and client-side source selection address different stages. Detecting suspicious uploads can reduce exposure, but it does not establish the resolver's intended behavior when two repositories contain the same name.

Likewise, a correctly selected private source does not prove that every package hosted there is trustworthy. The source rule establishes which authority supplies a dependency. Review of that authority and of the package remains a separate concern. The patch narrows a particular opportunity for substituting a public package where a private one was intended.

I regard source provenance as part of a dependency declaration's meaning. It deserves representation in both the search policy and the lockfile. Treating it as an incidental download detail would leave an important decision outside the configuration that developers read and review.

The distinction also changes how to interpret a successful install. It establishes that the resolver found a graph it accepts under its rules. To assess provenance, the recorded sources must agree with the application's intended repositories. An installation log that merely reports package names and versions does not answer that question completely.

Indirect dependencies make that assessment harder because application authors may never name them in a Gemfile. A direct dependency introduces its own requirements, and those requirements can introduce more packages. Source policy needs to remain meaningful as resolution traverses those relationships. Requiring every application author to flatten the entire graph into direct declarations would duplicate information already maintained by package authors and make dependency updates more difficult to review.

The specification also distinguishes a dependency available only in an unrelated pinned source from one available in a relevant source. A repository mentioned somewhere in the Gemfile is not automatically appropriate for every package. That distinction prevents a source block for an unrelated gem from silently expanding the candidate set for the rest of the graph. Repository configuration needs local meaning as well as a global default, which is why retaining the dependency relationship during source selection matters.

Bundler 2.2.10 supplies the source-priority change on February 15. Its release-tag changelog records the fix and the related lockfile representation work together. For the illustrated private dependency chain, the meaningful behavior is that an unrelated public version does not acquire priority merely by carrying a larger version number. The declared source context remains part of resolving the packages below the direct dependency.