diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py
index ca1044a16..86785d656 100644
--- a/IPython/html/base/handlers.py
+++ b/IPython/html/base/handlers.py
@@ -43,6 +43,10 @@ from IPython.external.decorator import decorator
from IPython.utils.path import filefind
from IPython.utils.jsonutil import date_default
+# UF_HIDDEN is a stat flag not defined in the stat module.
+# It is used by BSD to indicate hidden files.
+UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
+
#-----------------------------------------------------------------------------
# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
#-----------------------------------------------------------------------------
@@ -263,6 +267,7 @@ class IPythonHandler(AuthenticatedHandler):
raise web.HTTPError(400, u'Invalid JSON in body of request')
return model
+
class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
"""static files should only be accessible when logged in"""
@@ -272,7 +277,39 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
name = os.path.basename(path)
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
+
return web.StaticFileHandler.get(self, path)
+
+ def validate_absolute_path(self, root, absolute_path):
+ """Validate and return the absolute path.
+
+ Requires tornado 3.1
+
+ Adding to tornado's own handling, forbids the serving of hidden files.
+ """
+ abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
+ abs_root = os.path.abspath(root)
+ self.forbid_hidden(abs_root, abs_path)
+ return abs_path
+
+ def forbid_hidden(self, absolute_root, absolute_path):
+ """Raise 403 if a file is hidden or contained in a hidden directory.
+
+ Hidden is determined by either name starting with '.'
+ or the UF_HIDDEN flag as reported by stat
+ """
+ inside_root = absolute_path[len(absolute_root):]
+ if any(part.startswith('.') for part in inside_root.split(os.path.sep)):
+ raise web.HTTPError(403)
+
+ # check UF_HIDDEN on any location up to root
+ path = absolute_path
+ while path and path.startswith(absolute_root):
+ if os.stat(path).st_flags & UF_HIDDEN:
+ raise web.HTTPError(403)
+ path, _ = os.path.split(path)
+
+ return absolute_path
def json_errors(method):