Medium severity6.1NVD Advisory· Published Sep 21, 2017· Updated May 13, 2026
CVE-2015-4706
CVE-2015-4706
Description
Cross-site scripting (XSS) vulnerability in IPython 3.x before 3.2 allows remote attackers to inject arbitrary web script or HTML via vectors involving JSON error messages and the /api/contents path.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ipythonPyPI | >= 3.0.0, < 3.2.0 | 3.2.0 |
Affected products
2cpe:2.3:a:ipython:ipython:3.0.0:*:*:*:*:*:*:*+ 1 more
- cpe:2.3:a:ipython:ipython:3.0.0:*:*:*:*:*:*:*
- cpe:2.3:a:ipython:ipython:3.1.0:*:*:*:*:*:*:*
Patches
2c2078a53543eSet content type in json_error to application/json
1 file changed · +2 −0
IPython/html/base/handlers.py+2 −0 modified@@ -339,6 +339,7 @@ def wrapper(self, *args, **kwargs): message = e.log_message self.log.warn(message) self.set_status(e.status_code) + self.set_header('Content-Type', 'application/json') self.finish(json.dumps(dict(message=message))) except Exception: self.log.error("Unhandled error in API request", exc_info=True) @@ -348,6 +349,7 @@ def wrapper(self, *args, **kwargs): self.set_status(status) tb_text = ''.join(traceback.format_exception(t, value, tb)) reply = dict(message=message, traceback=tb_text) + self.set_header('Content-Type', 'application/json') self.finish(json.dumps(reply)) else: return result
11 files changed · +65 −38
IPython/html/base/handlers.py+34 −9 modified@@ -42,16 +42,24 @@ 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() : @@ -307,7 +315,22 @@ def write_error(self, status_code, **kwargs): 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): @@ -370,13 +393,15 @@ def wrapper(self, *args, **kwargs): 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) self.set_status(e.status_code) 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" @@ -399,7 +424,7 @@ def wrapper(self, *args, **kwargs): # 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 @@ -453,7 +478,7 @@ def validate_absolute_path(self, root, absolute_path): return super(FileFindHandler, self).validate_absolute_path(root, absolute_path) -class ApiVersionHandler(IPythonHandler): +class APIVersionHandler(APIHandler): @json_errors def get(self): @@ -524,5 +549,5 @@ def get(self, path=''): default_handlers = [ (r".*/", TrailingSlashHandler), - (r"api", ApiVersionHandler) + (r"api", APIVersionHandler) ]
IPython/html/services/clusters/handlers.py+4 −4 modified@@ -7,28 +7,28 @@ from tornado import web -from ...base.handlers import IPythonHandler +from ...base.handlers import APIHandler #----------------------------------------------------------------------------- # Cluster handlers #----------------------------------------------------------------------------- -class MainClusterHandler(IPythonHandler): +class MainClusterHandler(APIHandler): @web.authenticated def get(self): self.finish(json.dumps(self.cluster_manager.list_profiles())) -class ClusterProfileHandler(IPythonHandler): +class ClusterProfileHandler(APIHandler): @web.authenticated def get(self, profile): self.finish(json.dumps(self.cluster_manager.profile_info(profile))) -class ClusterActionHandler(IPythonHandler): +class ClusterActionHandler(APIHandler): @web.authenticated def post(self, profile, action):
IPython/html/services/config/handlers.py+2 −2 modified@@ -9,9 +9,9 @@ from tornado import web from IPython.utils.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
IPython/html/services/contents/handlers.py+4 −4 modified@@ -11,7 +11,7 @@ from IPython.utils.jsonutil import date_default from IPython.html.base.handlers import ( - IPythonHandler, json_errors, path_regex, + IPythonHandler, APIHandler, json_errors, path_regex, ) @@ -75,7 +75,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') @@ -257,7 +257,7 @@ def delete(self, path=''): self.finish() -class CheckpointsHandler(IPythonHandler): +class CheckpointsHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'POST') @@ -286,7 +286,7 @@ def post(self, path=''): self.finish(data) -class ModifyCheckpointsHandler(IPythonHandler): +class ModifyCheckpointsHandler(APIHandler): SUPPORTED_METHODS = ('POST', 'DELETE')
IPython/html/services/kernels/handlers.py+4 −4 modified@@ -13,12 +13,12 @@ from IPython.utils.py3compat import cast_unicode from IPython.html.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 IPython.core.release import kernel_protocol_version -class MainKernelHandler(IPythonHandler): +class MainKernelHandler(APIHandler): @web.authenticated @json_errors @@ -46,7 +46,7 @@ def post(self): self.finish(json.dumps(model)) -class KernelHandler(IPythonHandler): +class KernelHandler(APIHandler): SUPPORTED_METHODS = ('DELETE', 'GET') @@ -67,7 +67,7 @@ def delete(self, kernel_id): self.finish() -class KernelActionHandler(IPythonHandler): +class KernelActionHandler(APIHandler): @web.authenticated @json_errors
IPython/html/services/kernelspecs/handlers.py+3 −3 modified@@ -10,7 +10,7 @@ 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): @@ -40,7 +40,7 @@ def kernelspec_model(handler, name): ) return d -class MainKernelSpecHandler(IPythonHandler): +class MainKernelSpecHandler(APIHandler): SUPPORTED_METHODS = ('GET',) @web.authenticated @@ -62,7 +62,7 @@ def get(self): self.finish(json.dumps(model)) -class KernelSpecHandler(IPythonHandler): +class KernelSpecHandler(APIHandler): SUPPORTED_METHODS = ('GET',) @web.authenticated
IPython/html/services/kernels/tests/test_kernels_api.py+4 −2 modified@@ -67,7 +67,8 @@ def test_default_kernel(self): 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): @@ -80,7 +81,8 @@ def test_main_kernel_handler(self): 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
IPython/html/services/nbconvert/handlers.py+2 −2 modified@@ -2,9 +2,9 @@ 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
IPython/html/services/security/handlers.py+2 −2 modified@@ -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
IPython/html/services/sessions/handlers.py+3 −3 modified@@ -7,13 +7,13 @@ from tornado import web -from ...base.handlers import IPythonHandler, json_errors +from ...base.handlers import APIHandler, json_errors from IPython.utils.jsonutil import date_default from IPython.html.utils import url_path_join, url_escape from IPython.kernel.kernelspec import NoSuchKernel -class SessionRootHandler(IPythonHandler): +class SessionRootHandler(APIHandler): @web.authenticated @json_errors @@ -65,7 +65,7 @@ def post(self): self.set_status(201) self.finish(json.dumps(model, default=date_default)) -class SessionHandler(IPythonHandler): +class SessionHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
IPython/html/terminal/api_handlers.py+3 −3 modified@@ -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 @@ def post(self): self.finish(json.dumps({'name': name})) -class TerminalHandler(IPythonHandler): +class TerminalHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'DELETE') @web.authenticated
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/ipython/ipython/commit/7222bd53ad089a65fd610fab4626f9d0ab47dfcenvdPatchThird Party AdvisoryWEB
- github.com/ipython/ipython/commit/c2078a53543ed502efd968649fee1125e0eb549cnvdPatchThird Party AdvisoryWEB
- www.openwall.com/lists/oss-security/2015/06/22/7nvdMailing ListThird Party AdvisoryWEB
- www.securityfocus.com/bid/75328nvdThird Party AdvisoryVDB Entry
- bugzilla.redhat.com/show_bug.cginvdIssue TrackingThird Party AdvisoryVDB EntryWEB
- github.com/advisories/GHSA-q326-jhw3-699gghsaADVISORY
- ipython.org/ipython-doc/3/whatsnew/version3.htmlnvdRelease NotesVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2015-4706ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/ipython/PYSEC-2017-45.yamlghsaWEB
- web.archive.org/web/20200516112656/http://www.securityfocus.com/bid/75328ghsaWEB
News mentions
0No linked articles in our index yet.