diff --git a/notebook/auth/login.py b/notebook/auth/login.py index 6cf75b969..bf6c9a47a 100644 --- a/notebook/auth/login.py +++ b/notebook/auth/login.py @@ -25,7 +25,11 @@ class LoginHandler(IPythonHandler): def get(self): if self.current_user: - self.redirect(self.get_argument('next', default=self.base_url)) + next_url = self.get_argument('next', default=self.base_url) + if not next_url.startswith(self.base_url): + # require that next_url be absolute path within our path + next_url = self.base_url + self.redirect(next_url) else: self._render() @@ -47,8 +51,12 @@ class LoginHandler(IPythonHandler): else: self._render(message={'error': 'Invalid password'}) return - - self.redirect(self.get_argument('next', default=self.base_url)) + + next_url = self.get_argument('next', default=self.base_url) + if not next_url.startswith(self.base_url): + # require that next_url be absolute path within our path + next_url = self.base_url + self.redirect(next_url) @classmethod def get_user(cls, handler): diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 83cc41335..3ca50a46f 100644 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -40,16 +40,24 @@ sys_info = json.dumps(get_sys_info()) class AuthenticatedHandler(web.RequestHandler): """A RequestHandler with an authenticated user.""" + + @property + def content_security_policy(self): + """The default Content-Security-Policy header + + Can be overridden by defining Content-Security-Policy in settings['headers'] + """ + return '; '.join([ + "frame-ancestors 'self'", + # Make sure the report-uri is relative to the base_url + "report-uri " + url_path_join(self.base_url, csp_report_uri), + ]) def set_default_headers(self): headers = self.settings.get('headers', {}) if "Content-Security-Policy" not in headers: - headers["Content-Security-Policy"] = ( - "frame-ancestors 'self'; " - # Make sure the report-uri is relative to the base_url - "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";" - ) + headers["Content-Security-Policy"] = self.content_security_policy # Allow for overriding headers for header_name,value in headers.items() : @@ -301,7 +309,22 @@ class IPythonHandler(AuthenticatedHandler): html = self.render_template('error.html', **ns) self.write(html) - + + +class APIHandler(IPythonHandler): + """Base class for API handlers""" + + @property + def content_security_policy(self): + csp = '; '.join([ + super(APIHandler, self).content_security_policy, + "default-src 'none'", + ]) + return csp + + def finish(self, *args, **kwargs): + self.set_header('Content-Type', 'application/json') + return super(APIHandler, self).finish(*args, **kwargs) class Template404(IPythonHandler): @@ -364,6 +387,7 @@ def json_errors(method): try: result = yield gen.maybe_future(method(self, *args, **kwargs)) except web.HTTPError as e: + self.set_header('Content-Type', 'application/json') status = e.status_code message = e.log_message self.log.warn(message) @@ -371,6 +395,7 @@ def json_errors(method): reply = dict(message=message, reason=e.reason) self.finish(json.dumps(reply)) except Exception: + self.set_header('Content-Type', 'application/json') self.log.error("Unhandled error in API request", exc_info=True) status = 500 message = "Unknown server error" @@ -393,7 +418,7 @@ def json_errors(method): # to minimize subclass changes: HTTPError = web.HTTPError -class FileFindHandler(web.StaticFileHandler): +class FileFindHandler(IPythonHandler, web.StaticFileHandler): """subclass of StaticFileHandler for serving files from a search path""" # cache search results, don't search for files more than once @@ -447,7 +472,7 @@ class FileFindHandler(web.StaticFileHandler): return super(FileFindHandler, self).validate_absolute_path(root, absolute_path) -class ApiVersionHandler(IPythonHandler): +class APIVersionHandler(APIHandler): @json_errors def get(self): @@ -518,5 +543,5 @@ path_regex = r"(?P(?:(?:/[^/]+)+|/?))" default_handlers = [ (r".*/", TrailingSlashHandler), - (r"api", ApiVersionHandler) + (r"api", APIVersionHandler) ] diff --git a/notebook/services/config/handlers.py b/notebook/services/config/handlers.py index b4d72fe66..017c2c8ec 100644 --- a/notebook/services/config/handlers.py +++ b/notebook/services/config/handlers.py @@ -9,9 +9,9 @@ import errno from tornado import web from ipython_genutils.py3compat import PY3 -from ...base.handlers import IPythonHandler, json_errors +from ...base.handlers import APIHandler, json_errors -class ConfigHandler(IPythonHandler): +class ConfigHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH') @web.authenticated diff --git a/notebook/services/contents/handlers.py b/notebook/services/contents/handlers.py index 035e0ed36..2a0a4cca5 100644 --- a/notebook/services/contents/handlers.py +++ b/notebook/services/contents/handlers.py @@ -14,7 +14,7 @@ from notebook.utils import url_path_join, url_escape from jupyter_client.jsonutil import date_default from notebook.base.handlers import ( - IPythonHandler, json_errors, path_regex, + IPythonHandler, APIHandler, json_errors, path_regex, ) @@ -78,7 +78,7 @@ def validate_model(model, expect_content): ) -class ContentsHandler(IPythonHandler): +class ContentsHandler(APIHandler): SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE') @@ -260,7 +260,7 @@ class ContentsHandler(IPythonHandler): self.finish() -class CheckpointsHandler(IPythonHandler): +class CheckpointsHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'POST') @@ -289,7 +289,7 @@ class CheckpointsHandler(IPythonHandler): self.finish(data) -class ModifyCheckpointsHandler(IPythonHandler): +class ModifyCheckpointsHandler(APIHandler): SUPPORTED_METHODS = ('POST', 'DELETE') diff --git a/notebook/services/kernels/handlers.py b/notebook/services/kernels/handlers.py index a99e87579..863aa7fd8 100644 --- a/notebook/services/kernels/handlers.py +++ b/notebook/services/kernels/handlers.py @@ -16,12 +16,12 @@ from jupyter_client.jsonutil import date_default from ipython_genutils.py3compat import cast_unicode from notebook.utils import url_path_join, url_escape -from ...base.handlers import IPythonHandler, json_errors +from ...base.handlers import IPythonHandler, APIHandler, json_errors from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message from jupyter_client import protocol_version as client_protocol_version -class MainKernelHandler(IPythonHandler): +class MainKernelHandler(APIHandler): @web.authenticated @json_errors @@ -49,7 +49,7 @@ class MainKernelHandler(IPythonHandler): self.finish(json.dumps(model)) -class KernelHandler(IPythonHandler): +class KernelHandler(APIHandler): SUPPORTED_METHODS = ('DELETE', 'GET', 'OPTIONS') @@ -76,7 +76,7 @@ class KernelHandler(IPythonHandler): self.finish() -class KernelActionHandler(IPythonHandler): +class KernelActionHandler(APIHandler): @web.authenticated @json_errors diff --git a/notebook/services/kernels/tests/test_kernels_api.py b/notebook/services/kernels/tests/test_kernels_api.py index 0bc545c5d..eb6c36d64 100644 --- a/notebook/services/kernels/tests/test_kernels_api.py +++ b/notebook/services/kernels/tests/test_kernels_api.py @@ -69,7 +69,8 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.headers['Content-Security-Policy'], ( "frame-ancestors 'self'; " - "report-uri /api/security/csp-report;" + "report-uri /api/security/csp-report; " + "default-src 'none'" )) def test_main_kernel_handler(self): @@ -82,7 +83,8 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.headers['Content-Security-Policy'], ( "frame-ancestors 'self'; " - "report-uri /api/security/csp-report;" + "report-uri /api/security/csp-report; " + "default-src 'none'" )) # GET request diff --git a/notebook/services/kernelspecs/handlers.py b/notebook/services/kernelspecs/handlers.py index 57338de22..c033e7ce7 100644 --- a/notebook/services/kernelspecs/handlers.py +++ b/notebook/services/kernelspecs/handlers.py @@ -13,7 +13,7 @@ pjoin = os.path.join from tornado import web -from ...base.handlers import IPythonHandler, json_errors +from ...base.handlers import APIHandler, json_errors from ...utils import url_path_join def kernelspec_model(handler, name): @@ -43,7 +43,7 @@ def kernelspec_model(handler, name): ) return d -class MainKernelSpecHandler(IPythonHandler): +class MainKernelSpecHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'OPTIONS') @web.authenticated @@ -70,7 +70,7 @@ class MainKernelSpecHandler(IPythonHandler): self.finish() -class KernelSpecHandler(IPythonHandler): +class KernelSpecHandler(APIHandler): SUPPORTED_METHODS = ('GET',) @web.authenticated diff --git a/notebook/services/nbconvert/handlers.py b/notebook/services/nbconvert/handlers.py index f23ddc92b..7b55bdc2b 100644 --- a/notebook/services/nbconvert/handlers.py +++ b/notebook/services/nbconvert/handlers.py @@ -2,9 +2,9 @@ import json from tornado import web -from ...base.handlers import IPythonHandler, json_errors +from ...base.handlers import APIHandler, json_errors -class NbconvertRootHandler(IPythonHandler): +class NbconvertRootHandler(APIHandler): SUPPORTED_METHODS = ('GET',) @web.authenticated diff --git a/notebook/services/security/handlers.py b/notebook/services/security/handlers.py index f182a0e2b..c34ddf9ab 100644 --- a/notebook/services/security/handlers.py +++ b/notebook/services/security/handlers.py @@ -5,10 +5,10 @@ from tornado import gen, web -from ...base.handlers import IPythonHandler, json_errors +from ...base.handlers import APIHandler, json_errors from . import csp_report_uri -class CSPReportHandler(IPythonHandler): +class CSPReportHandler(APIHandler): '''Accepts a content security policy violation report''' @web.authenticated @json_errors diff --git a/notebook/services/sessions/handlers.py b/notebook/services/sessions/handlers.py index 3ecb670d5..888d518e9 100644 --- a/notebook/services/sessions/handlers.py +++ b/notebook/services/sessions/handlers.py @@ -10,13 +10,13 @@ import json from tornado import web -from ...base.handlers import IPythonHandler, json_errors +from ...base.handlers import APIHandler, json_errors from jupyter_client.jsonutil import date_default from notebook.utils import url_path_join, url_escape from jupyter_client.kernelspec import NoSuchKernel -class SessionRootHandler(IPythonHandler): +class SessionRootHandler(APIHandler): @web.authenticated @json_errors @@ -74,7 +74,7 @@ class SessionRootHandler(IPythonHandler): self.set_header('Access-Control-Allow-Headers', 'accept, content-type') self.finish() -class SessionHandler(IPythonHandler): +class SessionHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE') diff --git a/notebook/terminal/api_handlers.py b/notebook/terminal/api_handlers.py index 6895e7188..a7be0ae51 100644 --- a/notebook/terminal/api_handlers.py +++ b/notebook/terminal/api_handlers.py @@ -1,9 +1,9 @@ import json from tornado import web, gen -from ..base.handlers import IPythonHandler, json_errors +from ..base.handlers import APIHandler, json_errors from ..utils import url_path_join -class TerminalRootHandler(IPythonHandler): +class TerminalRootHandler(APIHandler): @web.authenticated @json_errors def get(self): @@ -19,7 +19,7 @@ class TerminalRootHandler(IPythonHandler): self.finish(json.dumps({'name': name})) -class TerminalHandler(IPythonHandler): +class TerminalHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'DELETE') @web.authenticated