diff --git a/CHANGES b/CHANGES index 54a65836..a48fb9e6 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,22 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.6.1 +------------- + +Bugfix release, released on December 31st 2010 + +- Fixed an issue where the default `OPTIONS` response was + not exposing all valid methods in the `Allow` header. +- Jinja2 template loading syntax now allows "./" in front of + a template load path. Previously this caused issues with + module setups. +- Fixed an issue where the subdomain setting for modules was + ignored for the static folder. +- Fixed a security problem that allowed clients to download arbitrary files + if the host server was a windows based operating system and the client + uses backslashes to escape the directory the files where exposed from. + Version 0.6 ----------- diff --git a/MANIFEST.in b/MANIFEST.in index 166311d4..3fef8b5b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include Makefile CHANGES LICENSE AUTHORS +recursive-include artwork * recursive-include tests * recursive-include examples * recursive-include docs * diff --git a/flask/app.py b/flask/app.py index 9b6b7836..72ed6292 100644 --- a/flask/app.py +++ b/flask/app.py @@ -19,7 +19,8 @@ from jinja2 import Environment from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.exceptions import HTTPException, InternalServerError, \ + MethodNotAllowed from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, _endpoint_from_view_func @@ -689,14 +690,28 @@ class Flask(_PackageBoundObject): # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if rule.provide_automatic_options and req.method == 'OPTIONS': - rv = self.response_class() - rv.allow.update(rule.methods) - return rv + return self._make_default_options_response() # otherwise dispatch to the handler for that endpoint return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) + def _make_default_options_response(self): + # This would be nicer in Werkzeug 0.7, which however currently + # is not released. Werkzeug 0.7 provides a method called + # allowed_methods() that returns all methods that are valid for + # a given path. + methods = [] + try: + _request_ctx_stack.top.url_adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass + rv = self.response_class() + rv.allow.update(methods) + return rv + def make_response(self, rv): """Converts the return value from a view function to a real response object that is an instance of :attr:`response_class`. diff --git a/flask/helpers.py b/flask/helpers.py index a2d951b4..3cc3a7f0 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -58,6 +58,13 @@ else: _tojson_filter = json.dumps +# what separators does this operating system provide that are not a slash? +# this is used by the send_from_directory function to ensure that nobody is +# able to access files from outside the filesystem. +_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] + if sep not in (None, '/')) + + def _endpoint_from_view_func(view_func): """Internal helper that returns the default endpoint for a given function. This always is the function name. @@ -386,7 +393,10 @@ def send_from_directory(directory, filename, **options): forwarded to :func:`send_file`. """ filename = posixpath.normpath(filename) - if filename.startswith(('/', '../')): + for sep in _os_alt_seps: + if sep in filename: + raise NotFound() + if os.path.isabs(filename) or filename.startswith('../'): raise NotFound() filename = os.path.join(directory, filename) if not os.path.isfile(filename): diff --git a/flask/module.py b/flask/module.py index e6e1ee36..1d860493 100644 --- a/flask/module.py +++ b/flask/module.py @@ -31,7 +31,8 @@ def _register_module(module, static_path): path = state.url_prefix + path state.app.add_url_rule(path + '/', endpoint='%s.static' % module.name, - view_func=module.send_static_file) + view_func=module.send_static_file, + subdomain=module.subdomain) return _register diff --git a/flask/templating.py b/flask/templating.py index db78c3af..4db03b75 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -8,6 +8,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import posixpath from jinja2 import BaseLoader, TemplateNotFound from .globals import _request_ctx_stack @@ -36,6 +37,9 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): + template = posixpath.normpath(template) + if template.startswith('../'): + raise TemplateNotFound(template) loader = None try: module, name = template.split('/', 1) diff --git a/setup.py b/setup.py index b97a33f7..3b4a3381 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.6', + version='0.6.1', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3000e41c..74aba698 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -120,6 +120,17 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] assert rv.data == '' + def test_options_on_multiple_rules(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + @app.route('/', methods=['PUT']) + def index_put(): + return 'Aha!' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + def test_request_dispatching(self): app = flask.Flask(__name__) @app.route('/') @@ -219,7 +230,13 @@ class BasicFunctionalityTestCase(unittest.TestCase): flask.session['test'] = 42 flask.session.permanent = permanent return '' - rv = app.test_client().get('/') + + @app.route('/test') + def test(): + return unicode(flask.session.permanent) + + client = app.test_client() + rv = client.get('/') assert 'set-cookie' in rv.headers match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) @@ -228,6 +245,9 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert expires.month == expected.month assert expires.day == expected.day + rv = client.get('/test') + assert rv.data == 'True' + permanent = False rv = app.test_client().get('/') assert 'set-cookie' in rv.headers @@ -756,6 +776,8 @@ class ModuleTestCase(unittest.TestCase): assert rv.data == 'Hello from the Frontend' rv = c.get('/admin/') assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/index2') + assert rv.data == 'Hello from the Admin' rv = c.get('/admin/static/test.txt') assert rv.data.strip() == 'Admin File' rv = c.get('/admin/static/css/test.css') @@ -795,6 +817,21 @@ class ModuleTestCase(unittest.TestCase): else: assert 0, 'expected exception' + # testcase for a security issue that may exist on windows systems + import os + import ntpath + old_path = os.path + os.path = ntpath + try: + try: + f('..\\__init__.py') + except NotFound: + pass + else: + assert 0, 'expected exception' + finally: + os.path = old_path + class SendfileTestCase(unittest.TestCase): @@ -1027,6 +1064,15 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://test.localhost/') assert rv.data == 'test index' + def test_module_static_path_subdomain(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'example.com' + from subdomaintestmodule import mod + app.register_module(mod) + c = app.test_client() + rv = c.get('/static/hello.txt', 'http://foo.example.com/') + assert rv.data.strip() == 'Hello Subdomain' + def test_subdomain_matching(self): app = flask.Flask(__name__) app.config['SERVER_NAME'] = 'localhost' diff --git a/tests/moduleapp/apps/admin/__init__.py b/tests/moduleapp/apps/admin/__init__.py index 98af2b26..b85b8024 100644 --- a/tests/moduleapp/apps/admin/__init__.py +++ b/tests/moduleapp/apps/admin/__init__.py @@ -7,3 +7,8 @@ admin = Module(__name__, url_prefix='/admin') @admin.route('/') def index(): return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') diff --git a/tests/subdomaintestmodule/__init__.py b/tests/subdomaintestmodule/__init__.py new file mode 100644 index 00000000..3c5e3583 --- /dev/null +++ b/tests/subdomaintestmodule/__init__.py @@ -0,0 +1,4 @@ +from flask import Module + + +mod = Module(__name__, 'foo', subdomain='foo') diff --git a/tests/subdomaintestmodule/static/hello.txt b/tests/subdomaintestmodule/static/hello.txt new file mode 100644 index 00000000..12e23c16 --- /dev/null +++ b/tests/subdomaintestmodule/static/hello.txt @@ -0,0 +1 @@ +Hello Subdomain