CVE-2017-18026 (published 2018-01-10T09:29Z)

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.

Severity: MEDIUM
CVSS v2 base score: 6.8 (AV:N/AC:M/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/redmine/redmine
commit_details: repo_url: https://github.com/redmine/redmine
author: Toshi MARUYAMA
author_date: 2017-12-07 11:38:23+00:00
msg: 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
repository: repo_url: https://github.com/redmine/redmine
repo_name: redmine/redmine
description: Mirror of redmine code source - Official Subversion repository is at https://svn.redmine.org/redmine - contact: @vividtone or maeda (at) farend (dot) jp
repo_language: Ruby
owner: redmine
file_changes: filename: mercurial_adapter.rb
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,8 @@ module Redmine
 
         # 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
@@ -286,8 +288,21 @@ module Redmine
           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}"

programming_language: Ruby
filename: mercurial_adapter_test.rb
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,7 @@ 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"
@@ -443,6 +444,24 @@ 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)

programming_language: Ruby
repo_url: https://github.com/redmine/redmine
commit_details: repo_url: https://github.com/redmine/redmine
author: Toshi MARUYAMA
author_date: 2017-12-07 11:38:47+00:00
msg: mercurial: work around faulty parsing of early command options (#27516)

Use -sVALUE and --long=VALUE instead of "-s VALUE" and "--long VALUE"
respectively.

Contributed by Yuya Nishihara.

git-svn-id: http://svn.redmine.org/redmine/trunk@17062 e93f8b46-1217-0410-a6f0-8f06a7374b81
repository: repo_url: https://github.com/redmine/redmine
repo_name: redmine/redmine
description: Mirror of redmine code source - Official Subversion repository is at https://svn.redmine.org/redmine - contact: @vividtone or maeda (at) farend (dot) jp
repo_language: Ruby
owner: redmine
file_changes: filename: mercurial_adapter.rb
change_type: ModificationType.MODIFY
diff: @@ -140,7 +140,7 @@ module Redmine
 
         def entries(path=nil, identifier=nil, options={})
           p1 = scm_iconv(@path_encoding, 'UTF-8', path)
-          manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
+          manifest = hg('rhmanifest', "-r#{CGI.escape(hgrev(identifier))}",
                         '--', CGI.escape(without_leading_slash(p1.to_s))) do |io|
             output = io.read.force_encoding('UTF-8')
             begin
@@ -181,9 +181,9 @@ module Redmine
         # Iterates the revisions by using a template file that
         # makes Mercurial produce a xml output.
         def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
-          hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
-          hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
-          hg_args << '--limit' << options[:limit] if options[:limit]
+          hg_args = ['log', '--debug', '-C', "--style=#{self.class.template_path}"]
+          hg_args << "-r#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
+          hg_args << "--limit=#{options[:limit]}" if options[:limit]
           hg_args << '--' << hgtarget(path) unless path.blank?
           log = hg(*hg_args) do |io|
             output = io.read.force_encoding('UTF-8')
@@ -224,19 +224,19 @@ module Redmine
 
         # Returns list of nodes in the specified branch
         def nodes_in_branch(branch, options={})
-          hg_args = ['rhlog', '--template', '{node}\n', '--rhbranch', CGI.escape(branch)]
-          hg_args << '--from' << CGI.escape(branch)
-          hg_args << '--to'   << '0'
-          hg_args << '--limit' << options[:limit] if options[:limit]
+          hg_args = ['rhlog', '--template={node}\n', "--rhbranch=#{CGI.escape(branch)}"]
+          hg_args << "--from=#{CGI.escape(branch)}"
+          hg_args << '--to=0'
+          hg_args << "--limit=#{options[:limit]}" if options[:limit]
           hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
         end
 
         def diff(path, identifier_from, identifier_to=nil)
           hg_args = %w|rhdiff|
           if identifier_to
-            hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
+            hg_args << "-r#{hgrev(identifier_to)}" << "-r#{hgrev(identifier_from)}"
           else
-            hg_args << '-c' << hgrev(identifier_from)
+            hg_args << "-c#{hgrev(identifier_from)}"
           end
           unless path.blank?
             p = scm_iconv(@path_encoding, 'UTF-8', path)
@@ -255,7 +255,7 @@ module Redmine
 
         def cat(path, identifier=nil)
           p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
-          hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), '--', hgtarget(p) do |io|
+          hg 'rhcat', "-r#{CGI.escape(hgrev(identifier))}", '--', hgtarget(p) do |io|
             io.binmode
             io.read
           end
@@ -266,7 +266,7 @@ module Redmine
         def annotate(path, identifier=nil)
           p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
           blame = Annotate.new
-          hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), '--', hgtarget(p) do |io|
+          hg 'rhannotate', '-ncu', "-r#{CGI.escape(hgrev(identifier))}", '--', hgtarget(p) do |io|
             io.each_line do |line|
               line.force_encoding('ASCII-8BIT')
               next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
@@ -304,7 +304,8 @@ module Redmine
           end
 
           repo_path = root_url || url
-          full_args = ['-R', repo_path, '--encoding', 'utf-8']
+          full_args = ["-R#{repo_path}", '--encoding=utf-8']
+          # don't use "--config=<value>" form for compatibility with ancient Mercurial
           full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
           full_args << '--config' << 'diff.git=false'
           full_args += args

programming_language: Ruby
filename: mercurial_adapter_test.rb
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,7 @@ 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
+  HgCommandAborted   = Redmine::Scm::Adapters::MercurialAdapter::HgCommandAborted
   HgCommandArgumentError = Redmine::Scm::Adapters::MercurialAdapter::HgCommandArgumentError
 
   REPOSITORY_PATH = repository_path('mercurial')
@@ -445,19 +446,18 @@ class MercurialAdapterTest < ActiveSupport::TestCase
     end
 
     def test_bad_early_options
-      assert_raise HgCommandArgumentError do
-        @adapter.diff('sources/welcome_controller.rb', '--config=alias.rhdiff=!xterm')
-      end
+      assert_nil @adapter.diff('sources/welcome_controller.rb',
+                               '--config=alias.rhdiff=!xterm')
       assert_raise HgCommandArgumentError do
         @adapter.entries('--debugger')
       end
-      assert_raise HgCommandArgumentError do
+      assert_raise HgCommandAborted do
         @adapter.revisions(nil, nil, nil, limit: '--repo=otherrepo')
       end
-      assert_raise HgCommandArgumentError do
+      assert_raise HgCommandAborted do
         @adapter.nodes_in_branch('default', limit: '--repository=otherrepo')
       end
-      assert_raise HgCommandArgumentError do
+      assert_raise HgCommandAborted do
         @adapter.nodes_in_branch('-Rotherrepo')
       end
     end

programming_language: Ruby
repo_url: https://github.com/redmine/redmine
commit_details: repo_url: https://github.com/redmine/redmine
author: Toshi MARUYAMA
author_date: 2017-12-07 11:38:35+00:00
msg: mercurial: separate command options and positional arguments with "--" (#27516)

We don't have much problems here thanks to hgtarget(path) and CGI.escape(),
which prepends a repository path and encodes "=" character respectively, but
it's better to not rely on the side effect of these functions.

Contributed by Yuya Nishihara.

git-svn-id: http://svn.redmine.org/redmine/trunk@17061 e93f8b46-1217-0410-a6f0-8f06a7374b81
repository: repo_url: https://github.com/redmine/redmine
repo_name: redmine/redmine
description: Mirror of redmine code source - Official Subversion repository is at https://svn.redmine.org/redmine - contact: @vividtone or maeda (at) farend (dot) jp
repo_language: Ruby
owner: redmine
file_changes: filename: mercurial_adapter.rb
change_type: ModificationType.MODIFY
diff: @@ -141,7 +141,7 @@ module Redmine
         def entries(path=nil, identifier=nil, options={})
           p1 = scm_iconv(@path_encoding, 'UTF-8', path)
           manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
-                        CGI.escape(without_leading_slash(p1.to_s))) do |io|
+                        '--', CGI.escape(without_leading_slash(p1.to_s))) do |io|
             output = io.read.force_encoding('UTF-8')
             begin
               parse_xml(output)['rhmanifest']['repository']['manifest']
@@ -184,7 +184,7 @@ module Redmine
           hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
           hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
           hg_args << '--limit' << options[:limit] if options[:limit]
-          hg_args << hgtarget(path) unless path.blank?
+          hg_args << '--' << hgtarget(path) unless path.blank?
           log = hg(*hg_args) do |io|
             output = io.read.force_encoding('UTF-8')
             begin
@@ -240,7 +240,7 @@ module Redmine
           end
           unless path.blank?
             p = scm_iconv(@path_encoding, 'UTF-8', path)
-            hg_args << CGI.escape(hgtarget(p))
+            hg_args << '--' << CGI.escape(hgtarget(p))
           end
           diff = []
           hg *hg_args do |io|
@@ -255,7 +255,7 @@ module Redmine
 
         def cat(path, identifier=nil)
           p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
-          hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
+          hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), '--', hgtarget(p) do |io|
             io.binmode
             io.read
           end
@@ -266,7 +266,7 @@ module Redmine
         def annotate(path, identifier=nil)
           p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
           blame = Annotate.new
-          hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
+          hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), '--', hgtarget(p) do |io|
             io.each_line do |line|
               line.force_encoding('ASCII-8BIT')
               next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}

programming_language: Ruby