diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index ae58e7a..fa58c2b 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -32,6 +32,8 @@ from IPython.utils.path import filefind from IPython.utils.py3compat import string_types from IPython.html.utils import is_hidden, url_path_join, url_escape +from IPython.html.services.security import csp_report_uri + #----------------------------------------------------------------------------- # Top-level handlers #----------------------------------------------------------------------------- @@ -45,17 +47,22 @@ class AuthenticatedHandler(web.RequestHandler): def set_default_headers(self): headers = self.settings.get('headers', {}) - if "X-Frame-Options" not in headers: - headers["X-Frame-Options"] = "SAMEORIGIN" + 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) + ";" + ) + # Allow for overriding headers for header_name,value in headers.items() : try: self.set_header(header_name, value) - except Exception: + except Exception as e: # tornado raise Exception (not a subclass) # if method is unsupported (websocket and Access-Control-Allow-Origin # for example, so just ignore) - pass + self.log.debug(e) def clear_login_cookie(self): self.clear_cookie(self.cookie_name) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index e21db1a..24d18e8 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -225,7 +225,7 @@ class NotebookWebApplication(web.Application): handlers.extend(load_handlers('services.sessions.handlers')) handlers.extend(load_handlers('services.nbconvert.handlers')) handlers.extend(load_handlers('services.kernelspecs.handlers')) - + handlers.extend(load_handlers('services.security.handlers')) handlers.append( (r"/nbextensions/(.*)", FileFindHandler, { 'path': settings['nbextensions_path'], diff --git a/IPython/html/services/kernels/tests/test_kernels_api.py b/IPython/html/services/kernels/tests/test_kernels_api.py index 8f29a07..b33142c 100644 --- a/IPython/html/services/kernels/tests/test_kernels_api.py +++ b/IPython/html/services/kernels/tests/test_kernels_api.py @@ -65,7 +65,10 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.status_code, 201) self.assertIsInstance(kern1, dict) - self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN") + self.assertEqual(r.headers['Content-Security-Policy'], ( + "frame-ancestors 'self'; " + "report-uri /api/security/csp-report;" + )) def test_main_kernel_handler(self): # POST request @@ -75,7 +78,10 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.status_code, 201) self.assertIsInstance(kern1, dict) - self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN") + self.assertEqual(r.headers['Content-Security-Policy'], ( + "frame-ancestors 'self'; " + "report-uri /api/security/csp-report;" + )) # GET request r = self.kern_api.list() diff --git a/IPython/html/services/security/__init__.py b/IPython/html/services/security/__init__.py new file mode 100644 index 0000000..9cf0d47 --- /dev/null +++ b/IPython/html/services/security/__init__.py @@ -0,0 +1,4 @@ +# URI for the CSP Report. Included here to prevent a cyclic dependency. +# csp_report_uri is needed both by the BaseHandler (for setting the report-uri) +# and by the CSPReportHandler (which depends on the BaseHandler). +csp_report_uri = r"/api/security/csp-report" diff --git a/IPython/html/services/security/handlers.py b/IPython/html/services/security/handlers.py new file mode 100644 index 0000000..18f7874 --- /dev/null +++ b/IPython/html/services/security/handlers.py @@ -0,0 +1,23 @@ +"""Tornado handlers for security logging.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from tornado import gen, web + +from ...base.handlers import IPythonHandler, json_errors +from . import csp_report_uri + +class CSPReportHandler(IPythonHandler): + '''Accepts a content security policy violation report''' + @web.authenticated + @json_errors + def post(self): + '''Log a content security policy violation report''' + csp_report = self.get_json_body() + self.log.warn("Content security violation: %s", + self.request.body.decode('utf8', 'replace')) + +default_handlers = [ + (csp_report_uri, CSPReportHandler) +] diff --git a/docs/source/whatsnew/development.rst b/docs/source/whatsnew/development.rst index b003dfa..87b41c5 100644 --- a/docs/source/whatsnew/development.rst +++ b/docs/source/whatsnew/development.rst @@ -180,16 +180,42 @@ Backwards incompatible changes .. DO NOT EDIT THIS LINE BEFORE RELEASE. INCOMPAT INSERTION POINT. -IFrame embedding -```````````````` +Content Security Policy +``````````````````````` -The IPython Notebook and its APIs by default will only be allowed to be -embedded in an iframe on the same origin. +The Content Security Policy is a web standard for adding a layer of security to +detect and mitigate certain classes of attacks, including Cross Site Scripting +(XSS) and data injection attacks. This was introduced into the notebook to +ensure that the IPython Notebook and its APIs (by default) can only be embedded +in an iframe on the same origin. -To override this, set ``headers[X-Frame-Options]`` to one of +Override ``headers['Content-Security-Policy']`` within your notebook +configuration to extend for alternate domains and security settings.:: -* DENY -* SAMEORIGIN -* ALLOW-FROM uri + c.NotebookApp.tornado_settings = { + 'headers': { + 'Content-Security-Policy': "frame-ancestors 'self'" + } + } -See `Mozilla's guide to X-Frame-Options `_ for more examples. +Example policies:: + + Content-Security-Policy: default-src 'self' https://*.jupyter.org + +Matches embeddings on any subdomain of jupyter.org, so long as they are served +over SSL. + +There is a `report-uri `_ endpoint available for logging CSP violations, located at +``/api/security/csp-report``. To use it, set ``report-uri`` as part of the CSP:: + + c.NotebookApp.tornado_settings = { + 'headers': { + 'Content-Security-Policy': "frame-ancestors 'self'; report-uri /api/security/csp-report" + } + } + +It simply provides the CSP report as a warning in IPython's logs. The default +CSP sets this report-uri relative to the ``base_url`` (not shown above). + +For a more thorough and accurate guide on Content Security Policies, check out +`MDN's Using Content Security Policy `_ for more examples.