Merge lp:~spiv/bzr/bug-400535 into lp:~bzr/bzr/trunk-old

Proposed by Andrew Bennetts
Status: Merged
Merged at revision: not available
Proposed branch: lp:~spiv/bzr/bug-400535
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 162 lines
To merge this branch: bzr merge lp:~spiv/bzr/bug-400535
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+9031@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Andrew Bennetts (spiv) wrote :

This patch fixes a serious bug in 1.16 and 1.17rc1. cmd_serve was refactored in
May to split out a “serve_bzr” function that takes care of creating a
ChrootServer for the specified directory and then starting a TCP or inet server
with that backing transport. Unfortunately, the refactoring introduced a bug
where the ChrootServer is ignored, and the plain LocalTransport is served
directly.

Thankfully, this doesn't appear to cause any security holes because invalid
paths like "../../../foo" are rejected by the smart server request handler
immediately, and requests that would try to walk up the filesystem to find a
BzrDir get stopped by the BzrDir.open jail that was recently introduced.

It's possible I've missed something and there is a way to exploit this bug to
trick an existing smart request into revealing data from outside the designated
directory, perhaps in conjunction with a server-side plugin, but so far I
haven't found any.

So, this patch fixes the essentially trivial bug, and adds a rather complicated
cmd_serve blackbox test that to prevent it regressing.

I suspect this bug also causes <https://bugs.launchpad.net/bzr/+bug/398199>, or
is at least contributing to it.

Because the bug is so serious, and because the fix is so obviously correct, this
fix should be included in 1.17 as well as bzr.dev.

-Andrew.

Revision history for this message
Robert Collins (lifeless) wrote :

 review +1

If backing_urls is in the local frame, then I suggest including it in
the new hook, if its an attribute on self, the the new hook signature is
fine.

Do we need to issue a new 1.16.x as well?

-Rob

review: Approve
Revision history for this message
Andrew Bennetts (spiv) wrote :

Robert Collins wrote:
> Review: Approve
> review +1
>
> If backing_urls is in the local frame, then I suggest including it in
> the new hook, if its an attribute on self, the the new hook signature is
> fine.

It's derivable from the server_obj, but not particularly conveniently so. I'll
add it.

> Do we need to issue a new 1.16.x as well?

I think that would be good, although perhaps with 1.17 so near it's not such a
big deal? I guess it's up to the RM and those that would build the installers
if they think it's worth the effort. I'm wavering between being +1 and +0.5 on
issuing a new 1.16.x for this.

-Andrew.

Revision history for this message
Robert Collins (lifeless) wrote :

I'd err on the side of more-effort where security concerns are present.

-Rob

Revision history for this message
Martin Pool (mbp) wrote :

2009/7/20 Robert Collins <email address hidden>:
> I'd err on the side of more-effort where security concerns are present.

How about if we talk to eg package maintainers before making applying
that effort, and see if they actually want a 1.16.1, or if perhaps
they just want the relevant patch?

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Robert Collins (lifeless) wrote :

On Mon, 2009-07-20 at 04:30 +0000, Martin Pool wrote:
> 2009/7/20 Robert Collins <email address hidden>:
> > I'd err on the side of more-effort where security concerns are present.
>
> How about if we talk to eg package maintainers before making applying
> that effort, and see if they actually want a 1.16.1, or if perhaps
> they just want the relevant patch?

Wearing my DD hat I'd want both - I can check that the new version is
really just the patch, then upload it without having to add a new patch
to the package which I'd just have to remove later.

-Rob

Revision history for this message
Wouter van Heyst (larstiq) wrote :

On Mon, Jul 20, 2009 at 01:10:17AM -0000, Andrew Bennetts wrote:
> Andrew Bennetts has proposed merging lp:~spiv/bzr/bug-400535 into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
> This patch fixes a serious bug in 1.16 and 1.17rc1. cmd_serve was refactored in
> May to split out a “serve_bzr” function that takes care of creating a
> ChrootServer for the specified directory and then starting a TCP or inet server
> with that backing transport. Unfortunately, the refactoring introduced a bug
> where the ChrootServer is ignored, and the plain LocalTransport is served
> directly.

Aha. I guess that explains why my smart-server locations.conf [chroot-*:///srv/bzr/]
section mysteriously stopped working with 1.16, and instead triggets on [/srv/bzr/].

I do think the latter one is much much nicer on the admin. Can I request
you retain the config behaviour for this merge, or do you want a
seperate bug for that?

Wouter van Heyst

Revision history for this message
Robert Collins (lifeless) wrote :

New bug please.

_Rob

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2009-07-17 05:32:50 +0000
+++ NEWS 2009-07-20 07:35:31 +0000
@@ -33,6 +33,9 @@
33* ``bzr mv`` no longer takes out branch locks, which allows it to work33* ``bzr mv`` no longer takes out branch locks, which allows it to work
34 when the branch is readonly. (Robert Collins, #216541)34 when the branch is readonly. (Robert Collins, #216541)
3535
36* ``bzr serve`` once again applies a ``ChrootServer`` to the given
37 directory before serving it. (Andrew Bennetts, #400535)
38
36* Fixed spurious "Source branch does not support stacking" warning when39* Fixed spurious "Source branch does not support stacking" warning when
37 pushing. (Andrew Bennetts, #388908)40 pushing. (Andrew Bennetts, #388908)
38 41
3942
=== modified file 'bzrlib/smart/server.py'
--- bzrlib/smart/server.py 2009-06-10 03:56:49 +0000
+++ bzrlib/smart/server.py 2009-07-20 07:35:31 +0000
@@ -111,6 +111,8 @@
111 pass111 pass
112 for hook in SmartTCPServer.hooks['server_started']:112 for hook in SmartTCPServer.hooks['server_started']:
113 hook(backing_urls, self.get_url())113 hook(backing_urls, self.get_url())
114 for hook in SmartTCPServer.hooks['server_started_ex']:
115 hook(backing_urls, self)
114 self._started.set()116 self._started.set()
115 try:117 try:
116 try:118 try:
@@ -214,6 +216,10 @@
214 "where backing_url is a list of URLs giving the "216 "where backing_url is a list of URLs giving the "
215 "server-specific directory locations, and public_url is the "217 "server-specific directory locations, and public_url is the "
216 "public URL for the directory being served.", (0, 16), None))218 "public URL for the directory being served.", (0, 16), None))
219 self.create_hook(HookPoint('server_started_ex',
220 "Called by the bzr server when it starts serving a directory. "
221 "server_started is called with (backing_urls, server_obj).",
222 (1, 17), None))
217 self.create_hook(HookPoint('server_stopped',223 self.create_hook(HookPoint('server_stopped',
218 "Called by the bzr server when it stops serving a directory. "224 "Called by the bzr server when it stops serving a directory. "
219 "server_stopped is called with the same parameters as the "225 "server_stopped is called with the same parameters as the "
@@ -313,7 +319,7 @@
313 from bzrlib.transport.chroot import ChrootServer319 from bzrlib.transport.chroot import ChrootServer
314 chroot_server = ChrootServer(transport)320 chroot_server = ChrootServer(transport)
315 chroot_server.setUp()321 chroot_server.setUp()
316 t = get_transport(chroot_server.get_url())322 transport = get_transport(chroot_server.get_url())
317 if inet:323 if inet:
318 smart_server = medium.SmartServerPipeStreamMedium(324 smart_server = medium.SmartServerPipeStreamMedium(
319 sys.stdin, sys.stdout, transport)325 sys.stdin, sys.stdout, transport)
@@ -322,8 +328,7 @@
322 host = medium.BZR_DEFAULT_INTERFACE328 host = medium.BZR_DEFAULT_INTERFACE
323 if port is None:329 if port is None:
324 port = medium.BZR_DEFAULT_PORT330 port = medium.BZR_DEFAULT_PORT
325 smart_server = SmartTCPServer(331 smart_server = SmartTCPServer(transport, host=host, port=port)
326 transport, host=host, port=port)
327 trace.note('listening on port: %s' % smart_server.port)332 trace.note('listening on port: %s' % smart_server.port)
328 # For the duration of this server, no UI output is permitted. note333 # For the duration of this server, no UI output is permitted. note
329 # that this may cause problems with blackbox tests. This should be334 # that this may cause problems with blackbox tests. This should be
330335
=== modified file 'bzrlib/tests/blackbox/test_serve.py'
--- bzrlib/tests/blackbox/test_serve.py 2009-06-10 03:56:49 +0000
+++ bzrlib/tests/blackbox/test_serve.py 2009-07-20 07:35:31 +0000
@@ -21,18 +21,22 @@
21import signal21import signal
22import subprocess22import subprocess
23import sys23import sys
24import thread
24import threading25import threading
2526
26from bzrlib import (27from bzrlib import (
27 errors,28 errors,
28 osutils,29 osutils,
29 revision as _mod_revision,30 revision as _mod_revision,
31 transport,
30 )32 )
31from bzrlib.branch import Branch33from bzrlib.branch import Branch
32from bzrlib.bzrdir import BzrDir34from bzrlib.bzrdir import BzrDir
33from bzrlib.errors import ParamikoNotPresent35from bzrlib.errors import ParamikoNotPresent
34from bzrlib.smart import medium36from bzrlib.smart import client, medium
37from bzrlib.smart.server import SmartTCPServer
35from bzrlib.tests import TestCaseWithTransport, TestSkipped38from bzrlib.tests import TestCaseWithTransport, TestSkipped
39from bzrlib.trace import mutter
36from bzrlib.transport import get_transport, remote40from bzrlib.transport import get_transport, remote
3741
3842
@@ -239,3 +243,77 @@
239 % bzr_remote_path],243 % bzr_remote_path],
240 self.command_executed)244 self.command_executed)
241245
246
247class TestCmdServeChrooting(TestCaseWithTransport):
248
249 def test_serve_tcp(self):
250 """'bzr serve' wraps the given --directory in a ChrootServer.
251
252 So requests that search up through the parent directories (like
253 find_repositoryV3) will give "not found" responses, rather than
254 InvalidURLJoin or jail break errors.
255 """
256 t = self.get_transport()
257 t.mkdir('server-root')
258 self.run_bzr_serve_then_func(
259 ['--port', '0', '--directory', t.local_abspath('server-root'),
260 '--allow-writes'],
261 self.when_server_started)
262 # The when_server_started method issued a find_repositoryV3 that should
263 # fail with 'norepository' because there are no repositories inside the
264 # --directory.
265 self.assertEqual(('norepository',), self.client_resp)
266
267 def run_bzr_serve_then_func(self, serve_args, func, *func_args,
268 **func_kwargs):
269 """Run 'bzr serve', and run the given func in a thread once the server
270 has started.
271
272 When 'func' terminates, the server will be terminated too.
273 """
274 # install hook
275 def on_server_start(backing_urls, tcp_server):
276 t = threading.Thread(
277 target=on_server_start_thread, args=(tcp_server,))
278 t.start()
279 def on_server_start_thread(tcp_server):
280 try:
281 # Run func
282 self.tcp_server = tcp_server
283 try:
284 func(*func_args, **func_kwargs)
285 except Exception, e:
286 # Log errors to make some test failures a little less
287 # mysterious.
288 mutter('func broke: %r', e)
289 finally:
290 # Then stop the server
291 mutter('interrupting...')
292 thread.interrupt_main()
293 SmartTCPServer.hooks.install_named_hook(
294 'server_started_ex', on_server_start,
295 'run_bzr_serve_then_func hook')
296 # start a TCP server
297 try:
298 self.run_bzr(['serve'] + list(serve_args))
299 except KeyboardInterrupt:
300 pass
301
302 def when_server_started(self):
303 # Connect to the TCP server and issue some requests and see what comes
304 # back.
305 client_medium = medium.SmartTCPClientMedium(
306 '127.0.0.1', self.tcp_server.port,
307 'bzr://localhost:%d/' % (self.tcp_server.port,))
308 smart_client = client._SmartClient(client_medium)
309 resp = smart_client.call('mkdir', 'foo', '')
310 resp = smart_client.call('BzrDirFormat.initialize', 'foo/')
311 try:
312 resp = smart_client.call('BzrDir.find_repositoryV3', 'foo/')
313 except errors.ErrorFromSmartServer, e:
314 resp = e.error_tuple
315 self.client_resp = resp
316 client_medium.disconnect()
317
318
319