Flask check: send_file() with a file handle

by Grayson Hardaway, Engineering @ r2c

Background

At r2c, our mission is to profoundly improve software security and reliability to safeguard human progress. We're writing custom program analysis checks and making them available for free and offline in Bento, our new developer tool focused on finding bugs that matter. These custom checks are in addition to tools already integrated into Bento, like Flake8, Bandit, and ESLint. The initial custom checks we’re adding to Bento analyze web frameworks/libraries such as Flask, Requests, and related web tools like Boto (the AWS SDK for Python).

The goal of these checks is to surface issues that you will end up fixing later—in code review, long-running integration tests, or security testing—as early as possible.

The Problem

send_file in Flask is a good example--it will throw a ValueError unless you provide a filename or mimetype, but you won't know that until you have the server running. This discussion on GitHub explains that this is new behavior as of Flask version 0.12 because the framework no longer infers the mimetype of file-like objects.

This check detects the use of open(filename, 'r') passed to flask.send_file() without the appropriate keyword args—either mimetype or attachment_filename. The keyword arguments prevent a ValueError from being thrown under these conditions. To see if this is happening in your codebase, run this in your Flask project directory:

pip3 install bento-cli && bento init

Here’s an example of what the check may find. The following snippet:

import flask
app = flask.Flask(**name**)
@app.route("/send_file")
def send_file():
f = open("test.db", 'r')
rv = flask.send_file(f)

Will throw this exception when you make a request:

[2019-10-30 10:25:14,695] ERROR in app: Exception on /send_file [GET][cut for brevity...]
File "example.py", line 5, in send_file
rv = flask.send_file(f)
File "/usr/local/lib/python3.7/site-packages/flask/helpers.py", line 593, in send_file
"Unable to infer MIME-type because no filename is available. "ValueError: Unable to infer MIME-type because no filename is available. Please set either attachment_filename, pass a filepath to filename_or_fp or set your own MIME-type via mimetype.

For the curious, the ValueError is raised here in the source.

Is This Check Any Good?

As outlined in our post on "Three Things Your Linter Shouldn't Tell You", we have a process we use to decide what checks are worthy of inclusion in Bento. Just doing the program analysis work isn't enough; we need to know that it will find issues a developer cares about, with good precision and recall.

We used r2c's program analysis platform to test out our check on real-world code. For this check, we used a set of 1.2k randomly sampled repositories on GitHub that use Flask. Often, running the check at scale yields some false positives we need to weed out (based on some faulty assumptions we have made!), such as this one:

return send_file(backend.get_filename(path))

We fixed this by examining whether the first argument is open(...) or a variable storing the value of open(...).

The current version of the check finds 109 instances of this pattern across 15 unique repositories out of the original 1.2k.

histogram

These findings are actually test code from old Flask versions that has been copied or forked--and this makes sense, given that real runtime errors are likely to be found and removed by someone running their app to test.

This check can help speed up development by catching an easy-to-make mistake before discovering it through dynamic testing of your app. It is available in Bento by default as of version 0.6.

Examples

This check detects the following cases:

# flask.send_file case
flask.send_file(open("file.txt", 'r'))

# With keyword args
flask.send_file(open("file.txt", 'r'), conditional=False)

# from flask import send_file case
flask.send_file(open("file.txt", 'r'))

# Variable resolution
fin = open("file.txt", 'r')
flask.send_file(fin)

This check considers the following cases acceptable:

# String argument for arg0
flask.send_file("/tmp/file.txt")

# Has a mimetype
flask.send_file(open("file.txt", 'r'), mimetype="text/plain")

# Has a attachment_filename
fin = open("file.txt", 'r')
flask.send_file(fin, as_attachment=True, attachment_filename="file.txt")

This check is available in Bento by default as of version 0.6. Bento is a free program analysis tool focused on finding bugs that matter to you.

References: