Bento check: Use jsonify() instead of json.dumps() in Flask

by Grayson Hardaway

> get this check now in Bento: pip3 install bento-cli && bento init

Get this check in Semgrep:

brew install semgrep && \
semgrep --config "https://semgrep.live/p/python-flask"

While r2c rolls out Flask checks for our program analysis tool, Bento, we are constantly seeking advice from community experts. We reached out to a core maintainer of Flask itself for suggestions on checks to write. The maintainer very graciously responded with a list, some of which are listed below:

  • Registering different routes with the same endpoint name (the function name by default). This will raise a runtime error but could be linted sooner.
  • Accessing request.form, request.json, request.data, etc. in a view that does not allow the POST method (or other method that accepts data).
  • Warn about relative paths, since the working directory might be different in production. Should use app.root_path or app.instance_path to generate absolute URLs.
  • Returning json.dumps() instead of jsonify().

We are incredibly grateful for the community's help and immediately queued up some of these checks for development. (If you want to suggest a check, please reach out to us! We decided to start with a common operation, the last one in the list above: returning jsonify().

jsonify() is a helper method provided by Flask to properly return JSON data. jsonify() returns a Response object with the application/json mimetype set, whereas json.dumps() simply returns a string of JSON data. This could lead to unintended results.

The check presented in this post detects Flask routes that return json.dumps() and encourages returning jsonify() instead.

This check will detect these cases:

import flask
import json
app = flask.Flask(__name__)

@app.route("/user")
def user():
    user_dict = get_user(request.args.get("id"))
    return json.dumps(user_dict)
## Method import
import flask
from json import dumps
app = flask.Flask(__name__)

@app.route("/user")
def user():
    user_dict = get_user(request.args.get("id"))
    return dumps(user_dict)

The check considers these cases acceptable:

## flask.jsonify
import flask
app = flask.Flask(__name__)

@app.route("/user")
def user():
    user_dict = get_user(request.args.get("id"))
    return flask.jsonify(user_dict)
## Import jsonify directly
from flask import Flask, jsonify
app = Flask(__name__)

@app.route("/user")
def user():
    user_dict = get_user(request.args.get("id"))
    return jsonify(user_dict)
## Not json.dumps
import json
from flask import Flask
from bson import dumps
app = Flask(__name__)

@app.route("/user")
def user():
    user_dict = get_user(request.args.get("id"))
    return dumps(user_dict)

We fine-tuned this check using our program analysis platform and discovered an edge case not accounted for in our original logic. This instance uses the dumps() method from bson.json_util instead of the json module. The original check assumed that if json was imported and dumps() was called, it was json.dumps(). However, this case demonstrated otherwise, so we improved our import resolution to make sure the dumps() method belonged to the json module.

It is incredibly important to test our static analysis on lots of real code to gauge its efficacy. We never would have found this edge case without the ability to run our analysis over source code en masse. All of our checks are calibrated this way, which bolsters our confidence in every check we release.

We tested the updated check on 715 Flask apps on GitHub, finding 120 instances of Flask routes returning json.dumps() across 27 repositories.

histogram for use jsonify

You can search for instances of this check in your own code, on your own machine, right now with Bento:

$ pip3 install bento-cli && bento init

References