Bento check: keeping your cookies safe in Flask

Ensure cookie settings are set securely in Flask

Grayson Hardaway
December 19th, 2019
Share

get this check now in Bento:

Get this check in Semgrep:

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

Cookies! 🍪🥠 Those little bits of data that I occasionally remember to purge from my browser.

In 2011, RFC6265 was accepted, which introduced new mechanisms for managing cookies. The document introduced the unhelpfully-named HttpOnly flag, which instructs a browser to forbid scripts from accessing the cookie. This mitigates XSS attacks that target cookies. RFC6265 also updated the definition for the Secure flag, which instructs the browser to only transmit the cookie over HTTPS. A new draft RFC adds even more cookie security settings, including the SameSite attribute, which permits or denies the browser the ability to send cookies with cross-site requests, mitigating CSRF attacks. Browsers have been adopting these settings for some time, and server frameworks like Flask support setting these attributes easily.

The check introduced in this post looks for Flask calls to set_cookie() where the secure, httponly, and samesite keyword arguments are not explicitly set.

# Example
response.set_cookie("name", "value", secure=True, httponly=True, samesite='Lax')

Flask's security recommendations suggest using Secure, HttpOnly, and SameSite where appropriate. Following this suggestion, the check detects cookies that are not set with these attributes. However, there are circumstances where some of these attributes do not need to be set. Therefore, the check permits explicit disabling. By doing this, it encourages developers to be explicit about how each cookie is handled.

The check will detect cases where none or some, but not all, of these keyword arguments are set:

# None set
from flask import make_response
@app.route("/none_set")
def none_set():
    response = make_response()
    response.set_cookie("cookie_name", "cookie_value")
    return response
# Some set
@app.route("/some_set")
def some_set():
    r = make_response()

    # some flags are set but not others
    r.set_cookie("cookie1", "cookie_value", secure=True)
    r.set_cookie("cookie2", "cookie_value", httponly=True)
    r.set_cookie("cookie3", "cookie_value", samesite='Lax')
    r.set_cookie("cookie4", "cookie_value", secure=True, httponly=True)
    r.set_cookie("cookie5", "cookie_value", httponly=True, samesite='Lax')
    return r

The check considers these cases acceptable:

import flask
@app.route("/")
def index():
    response = flask.make_response()
    response.set_cookie("cookie1", "cookie_value", secure=True, httponly=True, samesite='Lax')
    response.set_cookie("cookie2", "cookie_value", secure=True, httponly=True, samesite='Strict')
    response.set_cookie("cookie3", "cookie_value", secure=False, httponly=False, samesite=None)
    return response

I wondered if this check would fall under the category of annoying security recommendations and turned to our program analysis platform to see how frequently it fires. Of the 715 Flask apps on GitHub we used as a test set, the check fires in 18 repositories for a total of 66 instances. Reading the code, most of these projects would experience no drawback from using these secure cookie settings. This gives me some confidence that this check feels more like a friendly reminder instead of a bothersome decree! Speaking of friendly reminders, we are drafting PRs for these findings.

Histogram of r2c-flask-secure-set-cookie

You can check your own codebase for instances of this check right now with Bento:

$ pip3 install bento-cli && bento init

References

About

Semgrep lets security teams partner with developers and shift left organically, without introducing friction. Semgrep gives security teams confidence that they are only surfacing true, actionable issues to developers, and makes it easy for developers to fix these issues in their existing environments.