diff --git a/AUTHORS b/AUTHORS
index b0a4b6f388..d081d9f832 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -15,9 +15,11 @@ Patches and Suggestions
- Chris Grindstaff
- Christopher Grebs
- Daniel Neuhäuser
+- David Lord @davidism
- Edmond Burnett
- Florent Xicluna
- Georg Brandl
+- Jeff Widman @jeffwidman
- Justin Quick
- Kenneth Reitz
- Keyan Pishdadian
@@ -32,4 +34,3 @@ Patches and Suggestions
- Stephane Wirtel
- Thomas Schranz
- Zhao Xiaohong
-- David Lord @davidism
diff --git a/CHANGES b/CHANGES
index e3d9b0eb9c..41b054d729 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,9 @@ Version 1.0
(release date to be announced, codename to be selected)
+- Added support to serializing top-level arrays to :func:`flask.jsonify`. This
+ introduces a security risk in ancient browsers. See
+ :ref:`json_security` for details.
- Added before_render_template signal.
- Added `**kwargs` to :meth:`flask.Test.test_client` to support passing
additional keyword arguments to the constructor of
diff --git a/docs/security.rst b/docs/security.rst
index 55ac91fee5..3e97834dcc 100644
--- a/docs/security.rst
+++ b/docs/security.rst
@@ -95,81 +95,12 @@ the form validation framework, which does not exist in Flask.
JSON Security
-------------
-.. admonition:: ECMAScript 5 Changes
-
- Starting with ECMAScript 5 the behavior of literals changed. Now they
- are not constructed with the constructor of ``Array`` and others, but
- with the builtin constructor of ``Array`` which closes this particular
- attack vector.
-
-JSON itself is a high-level serialization format, so there is barely
-anything that could cause security problems, right? You can't declare
-recursive structures that could cause problems and the only thing that
-could possibly break are very large responses that can cause some kind of
-denial of service at the receiver's side.
-
-However there is a catch. Due to how browsers work the CSRF issue comes
-up with JSON unfortunately. Fortunately there is also a weird part of the
-JavaScript specification that can be used to solve that problem easily and
-Flask is kinda doing that for you by preventing you from doing dangerous
-stuff. Unfortunately that protection is only there for
-:func:`~flask.jsonify` so you are still at risk when using other ways to
-generate JSON.
-
-So what is the issue and how to avoid it? The problem are arrays at
-top-level in JSON. Imagine you send the following data out in a JSON
-request. Say that's exporting the names and email addresses of all your
-friends for a part of the user interface that is written in JavaScript.
-Not very uncommon:
-
-.. sourcecode:: javascript
-
- [
- {"username": "admin",
- "email": "admin@localhost"}
- ]
-
-And it is doing that of course only as long as you are logged in and only
-for you. And it is doing that for all ``GET`` requests to a certain URL,
-say the URL for that request is
-``http://example.com/api/get_friends.json``.
-
-So now what happens if a clever hacker is embedding this to his website
-and social engineers a victim to visiting his site:
-
-.. sourcecode:: html
-
-
-
-
-
-If you know a bit of JavaScript internals you might know that it's
-possible to patch constructors and register callbacks for setters. An
-attacker can use this (like above) to get all the data you exported in
-your JSON file. The browser will totally ignore the :mimetype:`application/json`
-mimetype if :mimetype:`text/javascript` is defined as content type in the script
-tag and evaluate that as JavaScript. Because top-level array elements are
-allowed (albeit useless) and we hooked in our own constructor, after that
-page loaded the data from the JSON response is in the `captured` array.
-
-Because it is a syntax error in JavaScript to have an object literal
-(``{...}``) toplevel an attacker could not just do a request to an
-external URL with the script tag to load up the data. So what Flask does
-is to only allow objects as toplevel elements when using
-:func:`~flask.jsonify`. Make sure to do the same when using an ordinary
-JSON generate function.
+In Flask 0.10 and lower, :func:`~flask.jsonify` did not serialize top-level
+arrays to JSON. This was because of a security vulnerability in ECMAScript 4.
+
+ECMAScript 5 closed this vulnerability, so only extremely old browsers are
+still vulnerable. All of these browsers have `other more serious
+vulnerabilities
+`_, so
+this behavior was changed and :func:`~flask.jsonify` now supports serializing
+arrays.
diff --git a/flask/json.py b/flask/json.py
index 885214d3b5..9bcaf72c5a 100644
--- a/flask/json.py
+++ b/flask/json.py
@@ -199,10 +199,22 @@ def htmlsafe_dump(obj, fp, **kwargs):
def jsonify(*args, **kwargs):
- """Creates a :class:`~flask.Response` with the JSON representation of
- the given arguments with an :mimetype:`application/json` mimetype. The
- arguments to this function are the same as to the :class:`dict`
- constructor.
+ """This function wraps :func:`dumps` to add a few enhancements that make
+ life easier. It turns the JSON output into a :class:`~flask.Response`
+ object with the :mimetype:`application/json` mimetype. For convenience, it
+ also converts multiple arguments into an array or multiple keyword arguments
+ into a dict. This means that both ``jsonify(1,2,3)`` and
+ ``jsonify([1,2,3])`` serialize to ``[1,2,3]``.
+
+ For clarity, the JSON serialization behavior has the following differences
+ from :func:`dumps`:
+
+ 1. Single argument: Passed straight through to :func:`dumps`.
+ 2. Multiple arguments: Converted to an array before being passed to
+ :func:`dumps`.
+ 3. Multiple keyword arguments: Converted to a dict before being passed to
+ :func:`dumps`.
+ 4. Both args and kwargs: Behavior undefined and will throw an exception.
Example usage::
@@ -222,8 +234,10 @@ def get_current_user():
"id": 42
}
- For security reasons only objects are supported toplevel. For more
- information about this, have a look at :ref:`json-security`.
+
+ .. versionchanged:: 1.0
+ Added support for serializing top-level arrays. This introduces a
+ security risk in ancient browsers. See :ref:`json_security` for details.
This function's response will be pretty printed if it was not requested
with ``X-Requested-With: XMLHttpRequest`` to simplify debugging unless
@@ -242,11 +256,21 @@ def get_current_user():
indent = 2
separators = (', ', ': ')
+ if args and kwargs:
+ raise TypeError(
+ "jsonify() behavior undefined when passed both args and kwargs"
+ )
+ elif len(args) == 1: # single args are passed directly to dumps()
+ data = args[0]
+ elif args: # convert multiple args into an array
+ data = list(args)
+ else: # convert kwargs to a dict
+ data = dict(kwargs)
+
# Note that we add '\n' to end of response
# (see https://github.com/mitsuhiko/flask/pull/1262)
rv = current_app.response_class(
- (dumps(dict(*args, **kwargs), indent=indent, separators=separators),
- '\n'),
+ (dumps(data, indent=indent, separators=separators), '\n'),
mimetype='application/json')
return rv
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 8d09327f78..620fd792e0 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -31,24 +31,6 @@ def has_encoding(name):
class TestJSON(object):
- def test_jsonify_date_types(self):
- """Test jsonify with datetime.date and datetime.datetime types."""
-
- test_dates = (
- datetime.datetime(1973, 3, 11, 6, 30, 45),
- datetime.date(1975, 1, 5)
- )
-
- app = flask.Flask(__name__)
- c = app.test_client()
-
- for i, d in enumerate(test_dates):
- url = '/datetest{0}'.format(i)
- app.add_url_rule(url, str(i), lambda val=d: flask.jsonify(x=val))
- rv = c.get(url)
- assert rv.mimetype == 'application/json'
- assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple())
-
def test_post_empty_json_adds_exception_to_response_content_in_debug(self):
app = flask.Flask(__name__)
app.config['DEBUG'] = True
@@ -103,8 +85,41 @@ def index():
content_type='application/json; charset=iso-8859-15')
assert resp.data == u'Hällo Wörld'.encode('utf-8')
- def test_jsonify(self):
- d = dict(a=23, b=42, c=[1, 2, 3])
+ def test_json_as_unicode(self):
+ app = flask.Flask(__name__)
+
+ app.config['JSON_AS_ASCII'] = True
+ with app.app_context():
+ rv = flask.json.dumps(u'\N{SNOWMAN}')
+ assert rv == '"\\u2603"'
+
+ app.config['JSON_AS_ASCII'] = False
+ with app.app_context():
+ rv = flask.json.dumps(u'\N{SNOWMAN}')
+ assert rv == u'"\u2603"'
+
+ def test_jsonify_basic_types(self):
+ """Test jsonify with basic types."""
+ # Should be able to use pytest parametrize on this, but I couldn't
+ # figure out the correct syntax
+ # https://pytest.org/latest/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions
+ test_data = (0, 1, 23, 3.14, 's', "longer string", True, False,)
+ app = flask.Flask(__name__)
+ c = app.test_client()
+ for i, d in enumerate(test_data):
+ url = '/jsonify_basic_types{0}'.format(i)
+ app.add_url_rule(url, str(i), lambda x=d: flask.jsonify(x))
+ rv = c.get(url)
+ assert rv.mimetype == 'application/json'
+ assert flask.json.loads(rv.data) == d
+
+ def test_jsonify_dicts(self):
+ """Test jsonify with dicts and kwargs unpacking."""
+ d = dict(
+ a=0, b=23, c=3.14, d='t', e='Hi', f=True, g=False,
+ h=['test list', 10, False],
+ i={'test':'dict'}
+ )
app = flask.Flask(__name__)
@app.route('/kw')
def return_kwargs():
@@ -118,18 +133,43 @@ def return_dict():
assert rv.mimetype == 'application/json'
assert flask.json.loads(rv.data) == d
- def test_json_as_unicode(self):
+ def test_jsonify_arrays(self):
+ """Test jsonify of lists and args unpacking."""
+ l = [
+ 0, 42, 3.14, 't', 'hello', True, False,
+ ['test list', 2, False],
+ {'test':'dict'}
+ ]
app = flask.Flask(__name__)
+ @app.route('/args_unpack')
+ def return_args_unpack():
+ return flask.jsonify(*l)
+ @app.route('/array')
+ def return_array():
+ return flask.jsonify(l)
+ c = app.test_client()
+ for url in '/args_unpack', '/array':
+ rv = c.get(url)
+ assert rv.mimetype == 'application/json'
+ assert flask.json.loads(rv.data) == l
- app.config['JSON_AS_ASCII'] = True
- with app.app_context():
- rv = flask.json.dumps(u'\N{SNOWMAN}')
- assert rv == '"\\u2603"'
+ def test_jsonify_date_types(self):
+ """Test jsonify with datetime.date and datetime.datetime types."""
- app.config['JSON_AS_ASCII'] = False
- with app.app_context():
- rv = flask.json.dumps(u'\N{SNOWMAN}')
- assert rv == u'"\u2603"'
+ test_dates = (
+ datetime.datetime(1973, 3, 11, 6, 30, 45),
+ datetime.date(1975, 1, 5)
+ )
+
+ app = flask.Flask(__name__)
+ c = app.test_client()
+
+ for i, d in enumerate(test_dates):
+ url = '/datetest{0}'.format(i)
+ app.add_url_rule(url, str(i), lambda val=d: flask.jsonify(x=val))
+ rv = c.get(url)
+ assert rv.mimetype == 'application/json'
+ assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple())
def test_json_attr(self):
app = flask.Flask(__name__)