apply pull request #1313

This should have resolved security issue#1261 -- gluon.tools.Expose
symlinks, however it does not deal well with the case where the base
exposed directory is '/'
This commit is contained in:
Chen Rotem Levy
2016-06-11 11:20:23 +03:00
parent 15a26c00b1
commit e020395bdc
2 changed files with 245 additions and 13 deletions
+42 -8
View File
@@ -6212,7 +6212,8 @@ class PluginManager(object):
class Expose(object):
def __init__(self, base=None, basename=None, extensions=None, allow_download=True):
def __init__(self, base=None, basename=None, extensions=None,
allow_download=True, follow_symlink_out=False):
"""
Examples:
Use as::
@@ -6230,10 +6231,19 @@ class Expose(object):
extensions: an optional list of file extensions for filtering
displayed files: e.g. `['.py', '.jpg']`
allow_download: whether to allow downloading selected files
follow_symlink_out: whether to follow symbolic links that points
points outside of `base`.
Warning: setting this to `True` might pose a security risk
if you don't also have complete control over writing
and file creation under `base`.
"""
current.session.forget()
base = base or os.path.join(current.request.folder, 'static')
self.follow_symlink_out = follow_symlink_out
self.base = self.normalize_path(
base or os.path.join(current.request.folder, 'static'))
self.basename = basename or current.request.function
self.base = base = os.path.realpath(base or os.path.join(current.request.folder, 'static'))
basename = basename or current.request.function
self.basename = basename
@@ -6241,19 +6251,23 @@ class Expose(object):
self.args = [arg for arg in current.request.raw_args.split('/') if arg]
else:
self.args = [arg for arg in current.request.args if arg]
filename = os.path.join(base, *self.args)
filename = os.path.join(self.base, *self.args)
if not os.path.exists(filename):
raise HTTP(404, "FILE NOT FOUND")
if not os.path.normpath(filename).startswith(base):
if not self.in_base(filename):
raise HTTP(401, "NOT AUTHORIZED")
if allow_download and not os.path.isdir(filename):
current.response.headers['Content-Type'] = contenttype(filename)
raise HTTP(200, open(filename, 'rb'), **current.response.headers)
self.path = path = os.path.join(filename, '*')
self.folders = [f[len(path) - 1:] for f in sorted(glob.glob(path))
if os.path.isdir(f) and not self.isprivate(f)]
self.filenames = [f[len(path) - 1:] for f in sorted(glob.glob(path))
if not os.path.isdir(f) and not self.isprivate(f)]
dirname_len = len(path) - 1
allowed = [f for f in sorted(glob.glob(path))
if not any([self.isprivate(f), self.issymlink_out(f)])]
self.folders = [f[dirname_len:]
for f in allowed if os.path.isdir(f)]
self.filenames = [f[dirname_len:]
for f in allowed if not os.path.isdir(f)]
if 'README' in self.filenames:
readme = open(os.path.join(filename, 'README')).read()
self.paragraph = MARKMIN(readme)
@@ -6280,6 +6294,26 @@ class Expose(object):
for folder in self.folders], **dict(_class="table")))
return ''
def in_base(self, f):
"""True if f/ is under self.base/
Where f ans slef.base are normalized paths
"""
s = lambda f: '%s%s' % (f, os.path.sep) # f -> f/
# The trailing '/' is for the case of '/foobar' in_base of '/foo':
# - becase '/foobar' starts with '/foo'
# - but '/foobar/' doesn't start with '/foo/'
return s(self.normalize_path(f)).startswith(s(self.base))
def normalize_path(self, f):
if self.follow_symlink_out:
return os.path.normpath(f)
else:
return os.path.realpath(f)
def issymlink_out(self, f):
"True if f is a symlink and is pointing outside of self.base"
return os.path.islink(f) and not self.in_base(f)
@staticmethod
def isprivate(f):
return 'private' in f or f.startswith('.') or f.endswith('~')