diff --git a/SOURCES/mercurial-cve-2016-3068.patch b/SOURCES/mercurial-cve-2016-3068.patch new file mode 100644 index 0000000..515a340 --- /dev/null +++ b/SOURCES/mercurial-cve-2016-3068.patch @@ -0,0 +1,99 @@ +From 837a69dc6ff77d8c93e73a64c067fe60530e4f1b Mon Sep 17 00:00:00 2001 +From: Mateusz Kwapich +Date: Sun, 20 Mar 2016 21:52:21 -0700 +Subject: [PATCH 1/6] subrepo: set GIT_ALLOW_PROTOCOL to limit git clone + protocols (SEC) + +CVE-2016-3068 (1/1) + +Git's git-remote-ext remote helper provides an ext:: URL scheme that +allows running arbitrary shell commands. This feature allows +implementing simple git smart transports with a single shell shell +command. However, git submodules could clone arbitrary URLs specified +in the .gitmodules file. This was reported as CVE-2015-7545 and fixed +in git v2.6.1. + +However, if a user directly clones a malicious ext URL, the git client +will still run arbitrary shell commands. + +Mercurial is similarly effected. Mercurial allows specifying git +repositories as subrepositories. Git ext:: URLs can be specified as +Mercurial subrepositories allowing arbitrary shell commands to be run +on `hg clone ...`. + +The Mercurial community would like to thank Blake Burkhart for +reporting this issue. The description of the issue is copied from +Blake's report. + +This commit changes submodules to pass the GIT_ALLOW_PROTOCOL env +variable to git commands with the same list of allowed protocols that +git submodule is using. + +When the GIT_ALLOW_PROTOCOL env variable is already set, we just pass it +to git without modifications. +--- + mercurial/subrepo.py | 5 +++++ + tests/test-subrepo-git.t | 34 ++++++++++++++++++++++++++++++++++ + 2 files changed, 39 insertions(+) + +diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py +index 3747377..7286f06 100644 +--- a/mercurial/subrepo.py ++++ b/mercurial/subrepo.py +@@ -1060,6 +1060,11 @@ class gitsubrepo(abstractsubrepo): + are not supported and very probably fail. + """ + self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands))) ++ if env is None: ++ env = os.environ.copy() ++ # fix for Git CVE-2015-7545 ++ if 'GIT_ALLOW_PROTOCOL' not in env: ++ env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh' + # unless ui.quiet is set, print git's stderr, + # which is mostly progress and useful info + errpipe = None +diff --git a/tests/test-subrepo-git.t b/tests/test-subrepo-git.t +index 9361193..24cb6a2 100644 +--- a/tests/test-subrepo-git.t ++++ b/tests/test-subrepo-git.t +@@ -558,3 +558,37 @@ traceback + #endif + + $ cd .. ++ ++test for Git CVE-2016-3068 ++ $ hg init malicious-subrepository ++ $ cd malicious-subrepository ++ $ echo "s = [git]ext::sh -c echo% pwned% >&2" > .hgsub ++ $ git init s ++ Initialized empty Git repository in $TESTTMP/tc/malicious-subrepository/s/.git/ ++ $ cd s ++ $ git commit --allow-empty -m 'empty' ++ [master (root-commit) 153f934] empty ++ $ cd .. ++ $ hg add .hgsub ++ $ hg commit -m "add subrepo" ++ $ cd .. ++ $ env -u GIT_ALLOW_PROTOCOL hg clone malicious-subrepository malicious-subrepository-protected ++ Cloning into '$TESTTMP/tc/malicious-subrepository-protected/s'... ++ fatal: transport 'ext' not allowed ++ updating to branch default ++ cloning subrepo s from ext::sh -c echo% pwned% >&2 ++ abort: git clone error 128 in s (in subrepo s) ++ [255] ++ ++whitelisting of ext should be respected (that's the git submodule behaviour) ++ $ env GIT_ALLOW_PROTOCOL=ext hg clone malicious-subrepository malicious-subrepository-clone-allowed ++ Cloning into '$TESTTMP/tc/malicious-subrepository-clone-allowed/s'... ++ pwned ++ fatal: Could not read from remote repository. ++ ++ Please make sure you have the correct access rights ++ and the repository exists. ++ updating to branch default ++ cloning subrepo s from ext::sh -c echo% pwned% >&2 ++ abort: git clone error 128 in s (in subrepo s) ++ [255] +-- +2.4.11 + diff --git a/SOURCES/mercurial-cve-2016-3069.patch b/SOURCES/mercurial-cve-2016-3069.patch new file mode 100644 index 0000000..66b0bb7 --- /dev/null +++ b/SOURCES/mercurial-cve-2016-3069.patch @@ -0,0 +1,283 @@ +diff --git a/hgext/convert/common.py b/hgext/convert/common.py +index e27346b..842ae71 100644 +--- a/hgext/convert/common.py ++++ b/hgext/convert/common.py +@@ -295,6 +295,9 @@ class commandline(object): + def _run2(self, cmd, *args, **kwargs): + return self._dorun(util.popen2, cmd, *args, **kwargs) + ++ def _run3(self, cmd, *args, **kwargs): ++ return self._dorun(util.popen3, cmd, *args, **kwargs) ++ + def _dorun(self, openfunc, cmd, *args, **kwargs): + cmdline = self._cmdline(cmd, *args, **kwargs) + self.ui.debug('running: %s\n' % (cmdline,)) +diff --git a/hgext/convert/git.py b/hgext/convert/git.py +index 3fd0884..8123b55 100644 +--- a/hgext/convert/git.py ++++ b/hgext/convert/git.py +@@ -7,11 +7,11 @@ + + import os + import subprocess +-from mercurial import util, config ++from mercurial import util, config, error + from mercurial.node import hex, nullid + from mercurial.i18n import _ + +-from common import NoRepo, commit, converter_source, checktool ++from common import NoRepo, commit, converter_source, checktool, commandline + + class submodule(object): + def __init__(self, path, node, url): +@@ -25,54 +25,32 @@ class submodule(object): + def hgsubstate(self): + return "%s %s" % (self.node, self.path) + +-class convert_git(converter_source): ++class convert_git(converter_source, commandline): + # Windows does not support GIT_DIR= construct while other systems + # cannot remove environment variable. Just assume none have + # both issues. +- if util.safehasattr(os, 'unsetenv'): +- def gitopen(self, s, err=None): +- prevgitdir = os.environ.get('GIT_DIR') +- os.environ['GIT_DIR'] = self.path +- try: +- if err == subprocess.PIPE: +- (stdin, stdout, stderr) = util.popen3(s) +- return stdout +- elif err == subprocess.STDOUT: +- return self.popen_with_stderr(s) +- else: +- return util.popen(s, 'rb') +- finally: +- if prevgitdir is None: +- del os.environ['GIT_DIR'] +- else: +- os.environ['GIT_DIR'] = prevgitdir +- else: +- def gitopen(self, s, err=None): +- if err == subprocess.PIPE: +- (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s)) +- return so +- elif err == subprocess.STDOUT: +- return self.popen_with_stderr(s) +- else: +- return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') +- +- def popen_with_stderr(self, s): +- p = subprocess.Popen(s, shell=True, bufsize=-1, +- close_fds=util.closefds, +- stdin=subprocess.PIPE, +- stdout=subprocess.PIPE, +- stderr=subprocess.STDOUT, +- universal_newlines=False, +- env=None) +- return p.stdout +- +- def gitread(self, s): +- fh = self.gitopen(s) +- data = fh.read() +- return data, fh.close() ++ ++ def _gitcmd(self, cmd, *args, **kwargs): ++ return cmd('--git-dir=%s' % self.path, *args, **kwargs) ++ ++ def gitrun0(self, *args, **kwargs): ++ return self._gitcmd(self.run0, *args, **kwargs) ++ ++ def gitrun(self, *args, **kwargs): ++ return self._gitcmd(self.run, *args, **kwargs) ++ ++ def gitrunlines0(self, *args, **kwargs): ++ return self._gitcmd(self.runlines0, *args, **kwargs) ++ ++ def gitrunlines(self, *args, **kwargs): ++ return self._gitcmd(self.runlines, *args, **kwargs) ++ ++ def gitpipe(self, *args, **kwargs): ++ return self._gitcmd(self._run3, *args, **kwargs) + + def __init__(self, ui, path, rev=None): + super(convert_git, self).__init__(ui, path, rev=rev) ++ commandline.__init__(self, ui, 'git') + + if os.path.isdir(path + "/.git"): + path += "/.git" +@@ -86,11 +64,11 @@ class convert_git(converter_source): + + def getheads(self): + if not self.rev: +- heads, ret = self.gitread('git rev-parse --branches --remotes') +- heads = heads.splitlines() ++ output, ret = self.gitrun('rev-parse', '--branches', '--remotes') ++ heads = output.splitlines() + else: +- heads, ret = self.gitread("git rev-parse --verify %s" % self.rev) +- heads = [heads[:-1]] ++ rawhead, ret = self.gitrun('rev-parse', '--verify', self.rev) ++ heads = [rawhead[:-1]] + if ret: + raise util.Abort(_('cannot retrieve git heads')) + return heads +@@ -98,7 +76,7 @@ class convert_git(converter_source): + def catfile(self, rev, type): + if rev == hex(nullid): + raise IOError +- data, ret = self.gitread("git cat-file %s %s" % (type, rev)) ++ data, ret = self.gitrun('cat-file', type, rev) + if ret: + raise util.Abort(_('cannot read %r object at %s') % (type, rev)) + return data +@@ -137,25 +115,28 @@ class convert_git(converter_source): + self.submodules.append(submodule(s['path'], '', s['url'])) + + def retrievegitmodules(self, version): +- modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules')) ++ modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules')) + if ret: + raise util.Abort(_('cannot read submodules config file in %s') % + version) + self.parsegitmodules(modules) + for m in self.submodules: +- node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path)) ++ node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path)) + if ret: + continue + m.node = node.strip() + + def getchanges(self, version): + self.modecache = {} +- fh = self.gitopen("git diff-tree -z --root -m -r %s" % version) ++ cmd = ['diff-tree','-z', '--root', '-m', '-r'] + [version] ++ output, status = self.gitrun(*cmd) ++ if status: ++ raise error.Abort(_('cannot read changes in %s') % version) + changes = [] + seen = set() + entry = None + subexists = False +- for l in fh.read().split('\x00'): ++ for l in output.split('\x00'): + if not entry: + if not l.startswith(':'): + continue +@@ -178,8 +159,6 @@ class convert_git(converter_source): + self.modecache[(f, h)] = (p and "x") or (s and "l") or "" + changes.append((f, h)) + entry = None +- if fh.close(): +- raise util.Abort(_('cannot read changes in %s') % version) + + if subexists: + self.retrievegitmodules(version) +@@ -224,12 +203,14 @@ class convert_git(converter_source): + def gettags(self): + tags = {} + alltags = {} +- fh = self.gitopen('git ls-remote --tags "%s"' % self.path, +- err=subprocess.STDOUT) ++ output, status = self.gitrunlines('ls-remote', '--tags', self.path) ++ ++ if status: ++ raise error.Abort(_('cannot read tags from %s') % self.path) + prefix = 'refs/tags/' + + # Build complete list of tags, both annotated and bare ones +- for line in fh: ++ for line in output: + line = line.strip() + if line.startswith("error:") or line.startswith("fatal:"): + raise util.Abort(_('cannot read tags from %s') % self.path) +@@ -237,8 +218,6 @@ class convert_git(converter_source): + if not tag.startswith(prefix): + continue + alltags[tag[len(prefix):]] = node +- if fh.close(): +- raise util.Abort(_('cannot read tags from %s') % self.path) + + # Filter out tag objects for annotated tag refs + for tag in alltags: +@@ -255,18 +234,20 @@ class convert_git(converter_source): + def getchangedfiles(self, version, i): + changes = [] + if i is None: +- fh = self.gitopen("git diff-tree --root -m -r %s" % version) +- for l in fh: ++ output, status = self.gitrunlines('diff-tree', '--root', '-m', ++ '-r', version) ++ if status: ++ raise error.Abort(_('cannot read changes in %s') % version) ++ for l in output: + if "\t" not in l: + continue + m, f = l[:-1].split("\t") + changes.append(f) + else: +- fh = self.gitopen('git diff-tree --name-only --root -r %s ' +- '"%s^%s" --' % (version, version, i + 1)) +- changes = [f.rstrip('\n') for f in fh] +- if fh.close(): +- raise util.Abort(_('cannot read changes in %s') % version) ++ output, status = self.gitrunlines('diff-tree', '--name-only', ++ '--root', '-r', version, ++ '%s^%s' % (version, i + 1), '--') ++ changes = [f.rstrip('\n') for f in output] + + return changes + +@@ -278,14 +259,14 @@ class convert_git(converter_source): + prefixlen = len(prefix) + + # factor two commands +- gitcmd = { 'remote/': 'git ls-remote --heads origin', +- '': 'git show-ref'} ++ gitcmd = { 'remote/': ['ls-remote', '--heads origin'], ++ '': ['show-ref']} + + # Origin heads + for reftype in gitcmd: + try: +- fh = self.gitopen(gitcmd[reftype], err=subprocess.PIPE) +- for line in fh: ++ output, status = self.gitrunlines(*gitcmd[reftype]) ++ for line in output: + line = line.strip() + rev, name = line.split(None, 1) + if not name.startswith(prefix): + +diff --git a/tests/test-convert-git.t b/tests/test-convert-git.t +index 21f18d2..7eb068b 100644 +--- a/tests/test-convert-git.t ++++ b/tests/test-convert-git.t +@@ -341,7 +341,7 @@ damage git repository by renaming a commit object + $ COMMIT_OBJ=1c/0ce3c5886f83a1d78a7b517cdff5cf9ca17bdd + $ mv git-repo4/.git/objects/$COMMIT_OBJ git-repo4/.git/objects/$COMMIT_OBJ.tmp + $ hg convert git-repo4 git-repo4-broken-hg 2>&1 | grep 'abort:' +- abort: cannot read tags from git-repo4/.git ++ abort: cannot retrieve number of commits in git-repo4/.git + $ mv git-repo4/.git/objects/$COMMIT_OBJ.tmp git-repo4/.git/objects/$COMMIT_OBJ + damage git repository by renaming a blob object + +@@ -356,3 +356,20 @@ damage git repository by renaming a tree object + $ mv git-repo4/.git/objects/$TREE_OBJ git-repo4/.git/objects/$TREE_OBJ.tmp + $ hg convert git-repo4 git-repo4-broken-hg 2>&1 | grep 'abort:' + abort: cannot read changes in 1c0ce3c5886f83a1d78a7b517cdff5cf9ca17bdd ++ ++test for escaping the repo name (CVE-2016-3069) ++ ++ $ git init '`echo pwned >COMMAND-INJECTION`' ++ Initialized empty Git repository in $TESTTMP/`echo pwned >COMMAND-INJECTION`/.git/ ++ $ cd '`echo pwned >COMMAND-INJECTION`' ++ $ git commit -q --allow-empty -m 'empty' ++ $ cd .. ++ $ hg convert '`echo pwned >COMMAND-INJECTION`' 'converted' ++ initializing destination converted repository ++ scanning source... ++ sorting... ++ converting... ++ 0 empty ++ updating bookmarks ++ $ test -f COMMAND-INJECTION ++ [1] diff --git a/SPECS/mercurial.spec b/SPECS/mercurial.spec index 1e59b30..2103e4e 100644 --- a/SPECS/mercurial.spec +++ b/SPECS/mercurial.spec @@ -3,7 +3,7 @@ Summary: Mercurial -- a distributed SCM Name: mercurial Version: 2.6.2 -Release: 4%{?dist} +Release: 6%{?dist} #Release: 1.rc1%{?dist} #%define upstreamversion %{version}-rc @@ -19,6 +19,11 @@ Patch0: mercurial-i18n.patch #Patch1: docutils-0.8.patch #Make hg-ssh's shebang pathname absolute (#987029) Patch2: mercurial-absolute-shebang.patch + +Patch3: mercurial-cve-2016-3068.patch +Patch4: mercurial-cve-2016-3069.patch + + BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root BuildRequires: python python-devel BuildRequires: emacs-nox emacs-el pkgconfig gettext python-docutils @@ -89,6 +94,8 @@ documentation. %patch0 -p0 #%patch1 -p1 %patch2 -p1 +%patch3 -p1 +%patch4 -p1 %build make all @@ -195,6 +202,12 @@ rm -rf $RPM_BUILD_ROOT ##cd tests && %{__python} run-tests.py %changelog +* Thu Apr 14 2016 Petr Stodulka - 2.6.2-6 +- fix previous patch for CVE-2016-3069 + +* Thu Apr 14 2016 Petr Stodulka - 2.6.2-5 +- Fix CVE-2016-3068 and CVE-2016-3069 + * Fri Jan 24 2014 Daniel Mach - 2.6.2-4 - Mass rebuild 2014-01-24