CVE-2017-18026: Insufficient Information (NVD-CWE-noinfo)
value: Redmine before 3.2.9, 3.3.x before 3.3.6, and 3.4.x before 3.4.4 does not block the --config and --debugger flags to the Mercurial hg program, which allows remote attackers to execute arbitrary commands (through the Mercurial adapter) via vectors involving a branch whose name begins with a --config= or --debugger= substring, a related issue to CVE-2017-17536.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 8.8
Published: 2018-01-10T09:29Z
Repository: https://github.com/redmine/redmine
Language: Ruby
Files changed: lib/redmine/scm/adapters/mercurial_adapter.rb, test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb
Commit date: 2017-12-07 11:38:23 +0000
Commit message: mercurial: reject malicious command argument (#27516)

We've got a security report from the Phabricator team, which basically says
--config and --debugger arguments can be injected anywhere to lead to an
arbitrary command execution.

https://secure.phabricator.com/rPa7921a4448093d00defa8bd18f35b8c8f8bf3314

This is a fundamental issue of the argument parsing rules in Mercurial, which
allows extensions to populate their parsing rules and such extensions can be
loaded by "--config extensions.<name>=". There's a chicken and egg problem.
We're working on hardening the parsing rules, but which won't come in by
default as it would be a behavior change.

This patch adds a verification to reject malicious command arguments as a
last ditch. The subsequent patches will fix the problem in more appropriate
way.

Contributed by Yuya Nishihara.

git-svn-id: http://svn.redmine.org/redmine/trunk@17060 e93f8b46-1217-0410-a6f0-8f06a7374b81

Diff: diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
index 064849f945..2c71213566 100644
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -30,10 +30,12 @@ module Redmine
         TEMPLATE_NAME = "hg-template"
         TEMPLATE_EXTENSION = "tmpl"
 
         # raised if hg command exited with error, e.g. unknown revision.
         class HgCommandAborted < CommandFailed; end
+        # raised if bad command argument detected before executing hg.
+        class HgCommandArgumentError < CommandFailed; end
 
         class << self
           def client_command
             @@bin    ||= HG_BIN
           end
@@ -284,12 +286,25 @@ module Redmine
           def format_identifier
             "#{revision}:#{scmid}"
           end
         end
 
+        # command options which may be processed earlier, by faulty parser in hg
+        HG_EARLY_BOOL_ARG = /^--(debugger|profile|traceback)$/
+        HG_EARLY_LIST_ARG = /^(--(config|cwd|repo(sitory)?)\b|-R)/
+        private_constant :HG_EARLY_BOOL_ARG, :HG_EARLY_LIST_ARG
+
         # Runs 'hg' command with the given args
         def hg(*args, &block)
+          # as of hg 4.4.1, early parsing of bool options is not terminated at '--'
+          if args.any? { |s| s =~ HG_EARLY_BOOL_ARG }
+            raise HgCommandArgumentError, "malicious command argument detected"
+          end
+          if args.take_while { |s| s != '--' }.any? { |s| s =~ HG_EARLY_LIST_ARG }
+            raise HgCommandArgumentError, "malicious command argument detected"
+          end
+
           repo_path = root_url || url
           full_args = ['-R', repo_path, '--encoding', 'utf-8']
           full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
           full_args << '--config' << 'diff.git=false'
           full_args += args
diff --git a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb
index 0512cc74ce..e0458ce55a 100644
--- a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb
+++ b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb
@@ -19,10 +19,11 @@ require File.expand_path('../../../../../../test_helper', __FILE__)
 
 class MercurialAdapterTest < ActiveSupport::TestCase
   HELPERS_DIR        = Redmine::Scm::Adapters::MercurialAdapter::HELPERS_DIR
   TEMPLATE_NAME      = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_NAME
   TEMPLATE_EXTENSION = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_EXTENSION
+  HgCommandArgumentError = Redmine::Scm::Adapters::MercurialAdapter::HgCommandArgumentError
 
   REPOSITORY_PATH = repository_path('mercurial')
   CHAR_1_HEX = "\xc3\x9c"
 
   if File.directory?(REPOSITORY_PATH)
@@ -441,10 +442,28 @@ class MercurialAdapterTest < ActiveSupport::TestCase
                                 ""
                               )
       assert_equal "UTF-8", adpt2.path_encoding
     end
 
+    def test_bad_early_options
+      assert_raise HgCommandArgumentError do
+        @adapter.diff('sources/welcome_controller.rb', '--config=alias.rhdiff=!xterm')
+      end
+      assert_raise HgCommandArgumentError do
+        @adapter.entries('--debugger')
+      end
+      assert_raise HgCommandArgumentError do
+        @adapter.revisions(nil, nil, nil, limit: '--repo=otherrepo')
+      end
+      assert_raise HgCommandArgumentError do
+        @adapter.nodes_in_branch('default', limit: '--repository=otherrepo')
+      end
+      assert_raise HgCommandArgumentError do
+        @adapter.nodes_in_branch('-Rotherrepo')
+      end
+    end
+
     private
 
     def test_hgversion_for(hgversion, version)
       @adapter.class.expects(:hgversion_from_command_line).returns(hgversion)
       if version

Fixed code:         # raised if bad command argument detected before executing hg.
        class HgCommandArgumentError < CommandFailed; end
        # command options which may be processed earlier, by faulty parser in hg
        HG_EARLY_BOOL_ARG = /^--(debugger|profile|traceback)$/
        HG_EARLY_LIST_ARG = /^(--(config|cwd|repo(sitory)?)\b|-R)/
        private_constant :HG_EARLY_BOOL_ARG, :HG_EARLY_LIST_ARG

          # as of hg 4.4.1, early parsing of bool options is not terminated at '--'
          if args.any? { |s| s =~ HG_EARLY_BOOL_ARG }
            raise HgCommandArgumentError, "malicious command argument detected"
          end
          if args.take_while { |s| s != '--' }.any? { |s| s =~ HG_EARLY_LIST_ARG }
            raise HgCommandArgumentError, "malicious command argument detected"
          end

  HgCommandArgumentError = Redmine::Scm::Adapters::MercurialAdapter::HgCommandArgumentError
    def test_bad_early_options
      assert_raise HgCommandArgumentError do
        @adapter.diff('sources/welcome_controller.rb', '--config=alias.rhdiff=!xterm')
      end
      assert_raise HgCommandArgumentError do
        @adapter.entries('--debugger')
      end
      assert_raise HgCommandArgumentError do
        @adapter.revisions(nil, nil, nil, limit: '--repo=otherrepo')
      end
      assert_raise HgCommandArgumentError do
        @adapter.nodes_in_branch('default', limit: '--repository=otherrepo')
      end
      assert_raise HgCommandArgumentError do
        @adapter.nodes_in_branch('-Rotherrepo')
      end
    end