07f2a6
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb
07f2a6
index 8e56d4a9bc..c37946b46c 100644
07f2a6
--- a/lib/bundler/definition.rb
07f2a6
+++ b/lib/bundler/definition.rb
07f2a6
@@ -910,6 +910,8 @@ def source_requirements
07f2a6
       # Load all specs from remote sources
07f2a6
       index
07f2a6
 
07f2a6
+      validate_dependency_confusion! unless disable_dependency_confusion_check?
07f2a6
+
07f2a6
       # Record the specs available in each gem's source, so that those
07f2a6
       # specs will be available later when the resolver knows where to
07f2a6
       # look for that gemspec (or its dependencies)
07f2a6
@@ -989,5 +991,112 @@ def equivalent_rubygems_remotes?(source)
07f2a6
 
07f2a6
       Bundler.settings[:allow_deployment_source_credential_changes] && source.equivalent_remotes?(sources.rubygems_remotes)
07f2a6
     end
07f2a6
+
07f2a6
+    def validate_dependency_confusion!
07f2a6
+      # Continue if there is a scoped repository in the remote case.
07f2a6
+      return unless @remote && sources.non_global_rubygems_sources.size > 0
07f2a6
+
07f2a6
+      # Raise an error unless all the scope repositories implement the dependency API.
07f2a6
+      # When there is a non-dependency API scoped repository, we cannot get
07f2a6
+      # indirect dependencies used in a `Gemfile`.
07f2a6
+      unless sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
07f2a6
+        non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?)
07f2a6
+        non_api_source_names_str = non_api_sources.map {|d| "  * #{d}" }.join("\n")
07f2a6
+
07f2a6
+        msg = String.new
07f2a6
+        msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n"
07f2a6
+        msg << non_api_source_names_str
07f2a6
+        msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \
07f2a6
+          "To resolve this warning, make sure you use gem servers that implement dependency APIs, " \
07f2a6
+          "such as gemstash or geminabox gem servers."
07f2a6
+        raise_error_or_warn_dependency_confusion(msg)
07f2a6
+        return
07f2a6
+      end
07f2a6
+
07f2a6
+      indirect_dep_names = indirect_dependency_names_in_non_global_rubygems_soruces
07f2a6
+      # Get all the gem names from the index made from the default source.
07f2a6
+      # default_source_dep_names = @index.sources.select(&:default_source_used?).map(&:spec_names).flatten
07f2a6
+      # Get all the gem names from each source.
07f2a6
+      all_spec_names_list = @index.sources.map(&:spec_names)
07f2a6
+
07f2a6
+      # Only include the indirect dependency gems on the scoped sources that
07f2a6
+      # also exist on another source. The gems are included in more than 2
07f2a6
+      # sources (the own source + another source). If the gems don't exist on
07f2a6
+      # the another source, the dependency confusion doesn't happen.
07f2a6
+      indirect_dep_names.select! do |name|
07f2a6
+        source_num = all_spec_names_list.select {|all_names| all_names.include?(name) }
07f2a6
+        source_num.size >= 2
07f2a6
+      end
07f2a6
+
07f2a6
+      # Raise an error if there is an indirect dependency.
07f2a6
+      if indirect_dep_names.size > 0
07f2a6
+        dep_names_str = indirect_dep_names.join(", ")
07f2a6
+        source_names_str = sources.non_global_rubygems_sources.map {|d| "  * #{d}" }.join("\n")
07f2a6
+
07f2a6
+        msg = String.new
07f2a6
+        msg << "Your Gemfile contains implicit dependency gems #{dep_names_str} on the scoped sources, namely:\n\n"
07f2a6
+        msg << source_names_str
07f2a6
+        msg << "\n\nUsing implicit dependency gems on the above sources may result in installing unexpected gems. "
07f2a6
+        msg << "To suppress this message, make sure you set the gems explicitly in the Gemfile."
07f2a6
+        raise_error_or_warn_dependency_confusion(msg)
07f2a6
+        return
07f2a6
+      end
07f2a6
+    end
07f2a6
+
07f2a6
+    def raise_error_or_warn_dependency_confusion(msg)
07f2a6
+      if warn_on_dependnecy_confusion?
07f2a6
+        Bundler.ui.warn msg
07f2a6
+      else
07f2a6
+        msg = "#{msg} Or set the environment variable BUNDLE_WARN_ON_DEPENDENCY_CONFUSION."
07f2a6
+        raise SecurityError, msg
07f2a6
+      end
07f2a6
+    end
07f2a6
+
07f2a6
+    def indirect_dependency_names_in_non_global_rubygems_soruces
07f2a6
+      # Indirect dependency gem names
07f2a6
+      indirect_dep_names = []
07f2a6
+      # Direct dependency gem names
07f2a6
+      direct_dep_names = @dependencies.map(&:name)
07f2a6
+
07f2a6
+      sources.non_global_rubygems_sources.each do |s|
07f2a6
+        # If the non dependency API source is used, the `dependency_names`
07f2a6
+        # returns gems not only used in the `Gemfile`, but also returns ones
07f2a6
+        # existing in the scoped source too. This method shouldn't be used with
07f2a6
+        # the non dependency API sources.
07f2a6
+        s.specs.dependency_names.each do |dep_name|
07f2a6
+          # Exclude direct dependency gems.
07f2a6
+          next if direct_dep_names.include?(dep_name)
07f2a6
+
07f2a6
+          s.specs.local_search(dep_name).each do |spec|
07f2a6
+            # Debug gems with unexpected `spec.class`.
07f2a6
+            Bundler.ui.debug "Found dependency gem #{dep_name} (#{spec.class}) in scoped sources."
07f2a6
+            # StubSpecification extending RemoteSpecification: the gems by
07f2a6
+            #   `gem list`. Exclude the gems.
07f2a6
+            # EndpointSpecification: gems returned by dependency API such as
07f2a6
+            #   geminabox
07f2a6
+            # RemoteSpecification: gems returned by non dependency API such as
07f2a6
+            #   gem server. This method cannot be executed with the non
07f2a6
+            #   dependency API sources.
07f2a6
+            indirect_dep_names << dep_name if spec.class == EndpointSpecification
07f2a6
+          end
07f2a6
+        end
07f2a6
+      end
07f2a6
+
07f2a6
+      indirect_dep_names.sort.uniq
07f2a6
+    end
07f2a6
+
07f2a6
+    # Print a warning instead of raising an error when this option is enabled.
07f2a6
+    # Don't use Bundler.settings to minimize the difference to backport easily
07f2a6
+    # and avoid additional tests.
07f2a6
+    def warn_on_dependnecy_confusion?
07f2a6
+      @warn_on_dependnecy_confusion ||= ENV["BUNDLE_WARN_ON_DEPENDENCY_CONFUSION"]
07f2a6
+    end
07f2a6
+
07f2a6
+    # Disable the dependency confusion check when this option is enabled.
07f2a6
+    # The option can be used as a workaround if the check logic is problematic
07f2a6
+    # in a case such as a performance issue.
07f2a6
+    def disable_dependency_confusion_check?
07f2a6
+      @disable_dependnecy_confusion_check ||= ENV["BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK"]
07f2a6
+    end
07f2a6
   end
07f2a6
 end
07f2a6
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
07f2a6
index 485b388a32..48a2ab736b 100644
07f2a6
--- a/lib/bundler/source/rubygems.rb
07f2a6
+++ b/lib/bundler/source/rubygems.rb
07f2a6
@@ -289,6 +289,10 @@ def dependency_names_to_double_check
07f2a6
         names
07f2a6
       end
07f2a6
 
07f2a6
+      def dependency_api_available?
07f2a6
+        api_fetchers.any?
07f2a6
+      end
07f2a6
+
07f2a6
     protected
07f2a6
 
07f2a6
       def credless_remotes
07f2a6
diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb
07f2a6
index ac2adacb3d..37869878ce 100644
07f2a6
--- a/lib/bundler/source_list.rb
07f2a6
+++ b/lib/bundler/source_list.rb
07f2a6
@@ -64,6 +64,10 @@ def rubygems_sources
07f2a6
       @rubygems_sources + [default_source]
07f2a6
     end
07f2a6
 
07f2a6
+    def non_global_rubygems_sources
07f2a6
+      @rubygems_sources
07f2a6
+    end
07f2a6
+
07f2a6
     def rubygems_remotes
07f2a6
       rubygems_sources.map(&:remotes).flatten.uniq
07f2a6
     end
07f2a6
diff --git a/spec/bundler/bundler/definition_dep_confusion_spec.rb b/spec/bundler/bundler/definition_dep_confusion_spec.rb
07f2a6
new file mode 100644
07f2a6
index 0000000000..9fee464960
07f2a6
--- /dev/null
07f2a6
+++ b/spec/bundler/bundler/definition_dep_confusion_spec.rb
07f2a6
@@ -0,0 +1,257 @@
07f2a6
+# frozen_string_literal: true
07f2a6
+
07f2a6
+require "bundler/definition"
07f2a6
+
07f2a6
+RSpec.describe Bundler::Definition do
07f2a6
+  before do
07f2a6
+    allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") }
07f2a6
+  end
07f2a6
+
07f2a6
+  let(:sources) { Bundler::SourceList.new }
07f2a6
+  subject { Bundler::Definition.new(nil, [], sources, []) }
07f2a6
+
07f2a6
+  describe "#validate_dependency_confusion!" do
07f2a6
+    before do
07f2a6
+      subject.instance_variable_set(:@remote, remote)
07f2a6
+    end
07f2a6
+
07f2a6
+    context "when it's not remote" do
07f2a6
+      let(:remote) { false }
07f2a6
+
07f2a6
+      it "should neither raise an error nor warn" do
07f2a6
+        expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
07f2a6
+        subject.send(:validate_dependency_confusion!)
07f2a6
+      end
07f2a6
+    end
07f2a6
+
07f2a6
+    context "when it's remote" do
07f2a6
+      before do
07f2a6
+        allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources)
07f2a6
+      end
07f2a6
+
07f2a6
+      let(:remote) { true }
07f2a6
+
07f2a6
+      context "when the number of non-global source is zero" do
07f2a6
+        let(:non_global_rubygems_sources) { [] }
07f2a6
+
07f2a6
+        it "should neither raise an error nor warn" do
07f2a6
+          expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
07f2a6
+          subject.send(:validate_dependency_confusion!)
07f2a6
+        end
07f2a6
+      end
07f2a6
+
07f2a6
+      context "when there are any non dependency API non global sources" do
07f2a6
+        let(:non_global_rubygems_sources) do
07f2a6
+          [
07f2a6
+            double("non-global-source-0", :dependency_api_available? => true, :to_s => "a"),
07f2a6
+            double("non-global-source-1", :dependency_api_available? => false, :to_s => "b"),
07f2a6
+            double("non-global-source-2", :dependency_api_available? => false, :to_s => "c"),
07f2a6
+          ]
07f2a6
+        end
07f2a6
+
07f2a6
+        it "should raise an error or warn" do
07f2a6
+          expect(subject).to receive(:raise_error_or_warn_dependency_confusion).with(<<-M.strip)
07f2a6
+Your Gemfile contains scoped sources that don't implement a dependency API, namely:
07f2a6
+
07f2a6
+  * b
07f2a6
+  * c
07f2a6
+
07f2a6
+Using the above gem servers may result in installing unexpected gems. To resolve this warning, make sure you use gem servers that implement dependency APIs, such as gemstash or geminabox gem servers.
07f2a6
+          M
07f2a6
+          subject.send(:validate_dependency_confusion!)
07f2a6
+        end
07f2a6
+      end
07f2a6
+
07f2a6
+      context "when all the non global sources implement dependency API" do
07f2a6
+        before do
07f2a6
+          allow(subject).to receive(:indirect_dependency_names_in_non_global_rubygems_soruces).and_return(indirect_dependency_names)
07f2a6
+          subject.instance_variable_set(:@index, index)
07f2a6
+        end
07f2a6
+
07f2a6
+        let(:non_global_rubygems_sources) do
07f2a6
+          [
07f2a6
+            double("non-global-source-0", :dependency_api_available? => true, :to_s => "a"),
07f2a6
+            double("non-global-source-1", :dependency_api_available? => true, :to_s => "b"),
07f2a6
+          ]
07f2a6
+        end
07f2a6
+
07f2a6
+        let(:index) { double("index", :sources => index_sources) }
07f2a6
+        let(:index_sources) do
07f2a6
+          [
07f2a6
+            double("index-source-1", :spec_names => ["a1", "a2"]),
07f2a6
+            double("index-source-2", :spec_names => ["a2", "b1", "b2"]),
07f2a6
+            double("index-source-3", :spec_names => ["b2"])
07f2a6
+          ]
07f2a6
+        end
07f2a6
+
07f2a6
+        context "when there is not an indirect dependency in the non global sources" do
07f2a6
+          let(:indirect_dependency_names) {[]}
07f2a6
+
07f2a6
+          it "should neither raise an error nor warn" do
07f2a6
+            expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
07f2a6
+            subject.send(:validate_dependency_confusion!)
07f2a6
+          end
07f2a6
+        end
07f2a6
+
07f2a6
+        context "when there is an indirect dependency in the non global sources" do
07f2a6
+
07f2a6
+          context "when the indirect dependency doesn't exist in another source" do
07f2a6
+            let(:indirect_dependency_names) {["a1", "b1"]}
07f2a6
+
07f2a6
+            it "should neither raise an error nor warn" do
07f2a6
+              expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
07f2a6
+              subject.send(:validate_dependency_confusion!)
07f2a6
+            end
07f2a6
+          end
07f2a6
+
07f2a6
+          context "when the indirect dependency also exists in anotehr source" do
07f2a6
+            let(:indirect_dependency_names) {["a1", "a2", "b2"]}
07f2a6
+
07f2a6
+            it "should raise an error or warn" do
07f2a6
+              expect(subject).to receive(:raise_error_or_warn_dependency_confusion).with(<<-M.strip)
07f2a6
+Your Gemfile contains implicit dependency gems a2, b2 on the scoped sources, namely:
07f2a6
+
07f2a6
+  * a
07f2a6
+  * b
07f2a6
+
07f2a6
+Using implicit dependency gems on the above sources may result in installing unexpected gems. To suppress this message, make sure you set the gems explicitly in the Gemfile.
07f2a6
+              M
07f2a6
+              subject.send(:validate_dependency_confusion!)
07f2a6
+            end
07f2a6
+          end
07f2a6
+        end
07f2a6
+      end
07f2a6
+    end
07f2a6
+  end
07f2a6
+
07f2a6
+  describe "#indirect_dependency_names_in_non_global_rubygems_soruces" do
07f2a6
+    before do
07f2a6
+      subject.instance_variable_set(:@dependencies, dependencies)
07f2a6
+      allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources)
07f2a6
+    end
07f2a6
+
07f2a6
+    # Direct dependencies
07f2a6
+    let(:dependencies) do
07f2a6
+      [
07f2a6
+        double("dependency-0", :name => "g0"),
07f2a6
+        double("dependency-1", :name => "g3")
07f2a6
+      ]
07f2a6
+    end
07f2a6
+    let(:non_global_rubygems_sources) do
07f2a6
+      [
07f2a6
+        double("non-global-source-0", :specs => index_0, :to_s => "s0"),
07f2a6
+        double("non-global-source-1", :specs => index_1, :to_s => "s1"),
07f2a6
+      ]
07f2a6
+    end
07f2a6
+    let(:index_0) do
07f2a6
+      # All the dependencies in the source-0.
07f2a6
+      index = double("index-0", :dependency_names => ["g0", "g1", "g2", "g5"])
07f2a6
+      allow(index).to receive(:local_search) do |query|
07f2a6
+        return_map = {
07f2a6
+          "g1" => [double("spec", :class => Bundler::StubSpecification, :to_s => "g1")],
07f2a6
+          "g2" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g2")],
07f2a6
+          "g5" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g5")]
07f2a6
+        }
07f2a6
+        return_map[query]
07f2a6
+      end
07f2a6
+      index
07f2a6
+    end
07f2a6
+    let(:index_1) do
07f2a6
+      # All the dependencies in the source-1.
07f2a6
+      index = double("index-1", :dependency_names => ["g3", "g4", "g5"])
07f2a6
+      allow(index).to receive(:local_search) do |query|
07f2a6
+        return_map = {
07f2a6
+          "g4" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g4")],
07f2a6
+          "g5" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g5")]
07f2a6
+        }
07f2a6
+        return_map[query]
07f2a6
+      end
07f2a6
+      index
07f2a6
+    end
07f2a6
+
07f2a6
+    it "should return only indirect dependencies of endpoint specification" do
07f2a6
+      expect(subject.send(:indirect_dependency_names_in_non_global_rubygems_soruces)).to eq(["g2", "g4", "g5"])
07f2a6
+    end
07f2a6
+  end
07f2a6
+
07f2a6
+  describe "#raise_error_or_warn_dependency_confusion" do
07f2a6
+    before do
07f2a6
+      allow(subject).to receive(:warn_on_dependnecy_confusion?).and_return(warn_on_dependnecy_confusion)
07f2a6
+    end
07f2a6
+
07f2a6
+    context "when #warn_on_dependnecy_confusion? returns false" do
07f2a6
+      let(:warn_on_dependnecy_confusion) { false }
07f2a6
+
07f2a6
+      it "should raise an error" do
07f2a6
+        expect(Bundler.ui).not_to receive(:warn)
07f2a6
+        expect do
07f2a6
+          subject.send(:raise_error_or_warn_dependency_confusion, "This is a message.")
07f2a6
+        end.to raise_error(Bundler::SecurityError, "This is a message. " \
07f2a6
+          "Or set the environment variable BUNDLE_WARN_ON_DEPENDENCY_CONFUSION.")
07f2a6
+      end
07f2a6
+    end
07f2a6
+
07f2a6
+    context "when #warn_on_dependnecy_confusion? returns true" do
07f2a6
+      let(:warn_on_dependnecy_confusion) { true }
07f2a6
+
07f2a6
+      it "should warn" do
07f2a6
+        expect(Bundler.ui).to receive(:warn).with(<<-W.strip)
07f2a6
+This is a message.
07f2a6
+W
07f2a6
+        subject.send(:raise_error_or_warn_dependency_confusion, "This is a message.")
07f2a6
+      end
07f2a6
+    end
07f2a6
+  end
07f2a6
+
07f2a6
+  describe "#warn_on_dependnecy_confusion?" do
07f2a6
+    context "when BUNDLE_WARN_ON_DEPENDENCY_CONFUSION is set" do
07f2a6
+      it "should return true" do
07f2a6
+        with_env({"BUNDLE_WARN_ON_DEPENDENCY_CONFUSION" => "1"}) do
07f2a6
+          expect(subject.send(:warn_on_dependnecy_confusion?)).to be_truthy
07f2a6
+        end
07f2a6
+      end
07f2a6
+    end
07f2a6
+
07f2a6
+    context "when BUNDLE_WARN_ON_DEPENDENCY_CONFUSION is not set" do
07f2a6
+      it "should return false" do
07f2a6
+        with_env({"BUNDLE_WARN_ON_DEPENDENCY_CONFUSION" => nil}) do
07f2a6
+          expect(subject.send(:warn_on_dependnecy_confusion?)).to be_falsy
07f2a6
+        end
07f2a6
+      end
07f2a6
+    end
07f2a6
+  end
07f2a6
+
07f2a6
+  describe "#disable_dependency_confusion_check?" do
07f2a6
+    context "when BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK is set" do
07f2a6
+      it "should return true" do
07f2a6
+        with_env({"BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK" => "1"}) do
07f2a6
+          expect(subject.send(:disable_dependency_confusion_check?)).to be_truthy
07f2a6
+        end
07f2a6
+      end
07f2a6
+    end
07f2a6
+
07f2a6
+    context "when BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK is not set" do
07f2a6
+      it "should return false" do
07f2a6
+        with_env({"BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK" => nil}) do
07f2a6
+          expect(subject.send(:disable_dependency_confusion_check?)).to be_falsy
07f2a6
+        end
07f2a6
+      end
07f2a6
+    end
07f2a6
+  end
07f2a6
+
07f2a6
+  def with_env(env={})
07f2a6
+    begin
07f2a6
+      tmp_env = {}
07f2a6
+      env.each do |key, value|
07f2a6
+        tmp_env[key] = ENV.delete key
07f2a6
+        ENV[key] = value
07f2a6
+      end
07f2a6
+
07f2a6
+      yield
07f2a6
+    ensure
07f2a6
+      tmp_env.each do |key, value|
07f2a6
+        ENV[key] = value
07f2a6
+      end
07f2a6
+    end
07f2a6
+  end
07f2a6
+end
07f2a6
-- 
07f2a6
2.31.1
07f2a6