Add support for serializing top-level arrays to JSON

Fix #170, #248, #510, #673, #1177
This commit is contained in:
Jeff Widman 2015-12-30 03:11:53 -08:00
parent f17e6061fc
commit daceb3e3a0
5 changed files with 114 additions and 115 deletions

View file

@ -95,81 +95,12 @@ the form validation framework, which does not exist in Flask.
JSON Security
-------------
.. admonition:: ECMAScript 5 Changes
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.
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
<script type=text/javascript>
var captured = [];
var oldArray = Array;
function Array() {
var obj = this, id = 0, capture = function(value) {
obj.__defineSetter__(id++, capture);
if (value)
captured.push(value);
};
capture();
}
</script>
<script type=text/javascript
src=http://example.com/api/get_friends.json></script>
<script type=text/javascript>
Array = oldArray;
// now we have all the data in the captured array.
</script>
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.
ECMAScript 5 closed this vulnerability, so only extremely old browsers are
still vulnerable. All of these browsers have `other more serious
vulnerabilities
<https://github.com/mitsuhiko/flask/issues/248#issuecomment-59934857>`_, so
this behavior was changed and :func:`~flask.jsonify` now supports serializing
arrays.