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