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):