diff --git a/mitogen/service.py b/mitogen/service.py index 55e69745..8016987c 100644 --- a/mitogen/service.py +++ b/mitogen/service.py @@ -756,6 +756,8 @@ class FileService(Service): super(FileService, self).__init__(router) #: Set of registered paths. self._paths = set() + #: Set of registered directory prefixes. + self._prefixes = set() #: Mapping of Stream->FileStreamState. self._state_by_stream = {} @@ -781,6 +783,22 @@ class FileService(Service): LOG.debug('%r: registering %r', self, path) self._paths.add(path) + @expose(policy=AllowParents()) + @arg_spec({ + 'path': mitogen.core.FsPathTypes, + }) + def register_prefix(self, path): + """ + Authorize a path and any subpaths for access by children. Repeat calls + with the same path has no effect. + + :param str path: + File path. + """ + if path not in self._prefixes: + LOG.debug('%r: registering prefix %r', self, path) + self._prefixes.add(path) + def _generate_stat(self, path): st = os.stat(path) if not stat.S_ISREG(st.st_mode): @@ -844,6 +862,24 @@ class FileService(Service): fp.close() state.jobs.pop(0) + def _prefix_is_authorized(self, path): + """ + Return the set of all possible directory prefixes for `path`. + :func:`os.path.abspath` is used to ensure the path is absolute. + + :param str path: + The path. + :returns: Set of prefixes. + """ + path = os.path.abspath(path) + while True: + if path in self._prefixes: + return True + if path == '/': + break + path = os.path.dirname(path) + return False + @expose(policy=AllowAny()) @no_reply() @arg_spec({ @@ -870,7 +906,7 @@ class FileService(Service): :raises Error: Unregistered path, or Sender did not match requestee context. """ - if path not in self._paths: + if path not in self._paths and not self._prefix_is_authorized(path): raise Error(self.unregistered_msg) if msg.src_id != sender.context.context_id: raise Error(self.context_mismatch_msg) diff --git a/tests/file_service_test.py b/tests/file_service_test.py new file mode 100644 index 00000000..2cc217a4 --- /dev/null +++ b/tests/file_service_test.py @@ -0,0 +1,81 @@ + +import unittest2 + +import mitogen.service + +import testlib + + +class FetchTest(testlib.RouterMixin, testlib.TestCase): + klass = mitogen.service.FileService + + def test_unauthorized(self): + service = self.klass(self.router) + e = self.assertRaises(mitogen.service.Error, + lambda: service.fetch( + path='/etc/shadow', + sender=None, + msg=mitogen.core.Message(), + ) + ) + + self.assertEquals(e.args[0], service.unregistered_msg) + + def test_path_authorized(self): + recv = mitogen.core.Receiver(self.router) + service = self.klass(self.router) + service.register('/etc/passwd') + self.assertEquals(None, service.fetch( + path='/etc/passwd', + sender=recv.to_sender(), + msg=mitogen.core.Message(), + )) + + def test_root_authorized(self): + recv = mitogen.core.Receiver(self.router) + service = self.klass(self.router) + service.register_prefix('/') + self.assertEquals(None, service.fetch( + path='/etc/passwd', + sender=recv.to_sender(), + msg=mitogen.core.Message(), + )) + + def test_prefix_authorized(self): + recv = mitogen.core.Receiver(self.router) + service = self.klass(self.router) + service.register_prefix('/etc') + self.assertEquals(None, service.fetch( + path='/etc/passwd', + sender=recv.to_sender(), + msg=mitogen.core.Message(), + )) + + def test_prefix_authorized_abspath_bad(self): + recv = mitogen.core.Receiver(self.router) + service = self.klass(self.router) + service.register_prefix('/etc') + self.assertEquals(None, service.fetch( + path='/etc/foo/bar/../../../passwd', + sender=recv.to_sender(), + msg=mitogen.core.Message(), + )) + + def test_prefix_authorized_abspath_bad(self): + recv = mitogen.core.Receiver(self.router) + service = self.klass(self.router) + service.register_prefix('/etc') + e = self.assertRaises(mitogen.service.Error, + lambda: service.fetch( + path='/etc/../shadow', + sender=recv.to_sender(), + msg=mitogen.core.Message(), + ) + ) + + self.assertEquals(e.args[0], service.unregistered_msg) + + + +if __name__ == '__main__': + unittest2.main()