Security Considerations
Web applications usually face all kinds of security problems and it’s veryhard to get everything right. Flask tries to solve a few of these thingsfor you, but there are a couple more you have to take care of yourself.
Cross-Site Scripting (XSS)
Cross site scripting is the concept of injecting arbitrary HTML (and withit JavaScript) into the context of a website. To remedy this, developershave to properly escape text so that it cannot include arbitrary HTMLtags. For more information on that have a look at the Wikipedia articleon Cross-Site Scripting.
Flask configures Jinja2 to automatically escape all values unlessexplicitly told otherwise. This should rule out all XSS problems causedin templates, but there are still other places where you have to becareful:
generating HTML without the help of Jinja2
calling
Markup
on data submitted by userssending out HTML from uploaded files, never do that, use the
Content-Disposition: attachment
header to prevent that problem.sending out textfiles from uploaded files. Some browsers are usingcontent-type guessing based on the first few bytes so users couldtrick a browser to execute HTML.
Another thing that is very important are unquoted attributes. WhileJinja2 can protect you from XSS issues by escaping HTML, there is onething it cannot protect you from: XSS by attribute injection. To counterthis possible attack vector, be sure to always quote your attributes witheither double or single quotes when using Jinja expressions in them:
- <input value="{{ value }}">
Why is this necessary? Because if you would not be doing that, anattacker could easily inject custom JavaScript handlers. For example anattacker could inject this piece of HTML+JavaScript:
- onmouseover=alert(document.cookie)
When the user would then move with the mouse over the input, the cookiewould be presented to the user in an alert window. But instead of showingthe cookie to the user, a good attacker might also execute any otherJavaScript code. In combination with CSS injections the attacker mighteven make the element fill out the entire page so that the user wouldjust have to have the mouse anywhere on the page to trigger the attack.
There is one class of XSS issues that Jinja’s escaping does not protectagainst. The a
tag’s href
attribute can contain a javascript: URI,which the browser will execute when clicked if not secured properly.
- <a href="{{ value }}">click here</a>
- <a href="javascript:alert('unsafe');">click here</a>
To prevent this, you’ll need to set the Content Security Policy (CSP) response header.
Cross-Site Request Forgery (CSRF)
Another big problem is CSRF. This is a very complex topic and I won’toutline it here in detail just mention what it is and how to theoreticallyprevent it.
If your authentication information is stored in cookies, you have implicitstate management. The state of “being logged in” is controlled by acookie, and that cookie is sent with each request to a page.Unfortunately that includes requests triggered by 3rd party sites. If youdon’t keep that in mind, some people might be able to trick yourapplication’s users with social engineering to do stupid things withoutthem knowing.
Say you have a specific URL that, when you sent POST
requests to willdelete a user’s profile (say http://example.com/user/delete
). If anattacker now creates a page that sends a post request to that page withsome JavaScript they just have to trick some users to load that page andtheir profiles will end up being deleted.
Imagine you were to run Facebook with millions of concurrent users andsomeone would send out links to images of little kittens. When userswould go to that page, their profiles would get deleted while they arelooking at images of fluffy cats.
How can you prevent that? Basically for each request that modifiescontent on the server you would have to either use a one-time token andstore that in the cookie and also transmit it with the form data.After receiving the data on the server again, you would then have tocompare the two tokens and ensure they are equal.
Why does Flask not do that for you? The ideal place for this to happen isthe form validation framework, which does not exist in Flask.
JSON Security
In Flask 0.10 and lower, jsonify()
did not serialize top-levelarrays to JSON. This was because of a security vulnerability in ECMAScript 4.
ECMAScript 5 closed this vulnerability, so only extremely old browsers arestill vulnerable. All of these browsers have other more seriousvulnerabilities, sothis behavior was changed and jsonify()
now supports serializingarrays.
Security Headers
Browsers recognize various response headers in order to control security. Werecommend reviewing each of the headers below for use in your application.The Flask-Talisman extension can be used to manage HTTPS and the securityheaders for you.
HTTP Strict Transport Security (HSTS)
Tells the browser to convert all HTTP requests to HTTPS, preventingman-in-the-middle (MITM) attacks.
- response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
Content Security Policy (CSP)
Tell the browser where it can load various types of resource from. This headershould be used whenever possible, but requires some work to define the correctpolicy for your site. A very strict policy would be:
- response.headers['Content-Security-Policy'] = "default-src 'self'"
X-Content-Type-Options
Forces the browser to honor the response content type instead of trying todetect it, which can be abused to generate a cross-site scripting (XSS)attack.
- response.headers['X-Content-Type-Options'] = 'nosniff'
X-Frame-Options
Prevents external sites from embedding your site in an iframe
. Thisprevents a class of attacks where clicks in the outer frame can be translatedinvisibly to clicks on your page’s elements. This is also known as“clickjacking”.
- response.headers['X-Frame-Options'] = 'SAMEORIGIN'
X-XSS-Protection
The browser will try to prevent reflected XSS attacks by not loading the pageif the request contains something that looks like JavaScript and the responsecontains the same data.
- response.headers['X-XSS-Protection'] = '1; mode=block'
Set-Cookie options
These options can be added to a Set-Cookie
header to improve theirsecurity. Flask has configuration options to set these on the session cookie.They can be set on other cookies too.
Secure
limits cookies to HTTPS traffic only.HttpOnly
protects the contents of cookies from being read withJavaScript.SameSite
restricts how cookies are sent with requests fromexternal sites. Can be set to'Lax'
(recommended) or'Strict'
.Lax
prevents sending cookies with CSRF-prone requests fromexternal sites, such as submitting a form.Strict
prevents sendingcookies with all external requests, including following regular links.
- app.config.update(
- SESSION_COOKIE_SECURE=True,
- SESSION_COOKIE_HTTPONLY=True,
- SESSION_COOKIE_SAMESITE='Lax',
- )
- response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax')
Specifying Expires
or Max-Age
options, will remove the cookie afterthe given time, or the current time plus the age, respectively. If neitheroption is set, the cookie will be removed when the browser is closed.
- # cookie expires after 10 minutes
- response.set_cookie('snakes', '3', max_age=600)
For the session cookie, if session.permanent
is set, then PERMANENT_SESSION_LIFETIME
is used to set the expiration.Flask’s default cookie implementation validates that the cryptographicsignature is not older than this value. Lowering this value may help mitigatereplay attacks, where intercepted cookies can be sent at a later time.
- app.config.update(
- PERMANENT_SESSION_LIFETIME=600
- )
- @app.route('/login', methods=['POST'])
- def login():
- ...
- session.clear()
- session['user_id'] = user.id
- session.permanent = True
- ...
Use itsdangerous.TimedSerializer
to sign and validate other cookievalues (or any values that need secure signatures).
HTTP Public Key Pinning (HPKP)
This tells the browser to authenticate with the server using only the specificcertificate key to prevent MITM attacks.
Warning
Be careful when enabling this, as it is very difficult to undo if you set upor upgrade your key incorrectly.