Security Patterns¶
Danger
Be aware that starting in Flask 2.2.0, they recommend extensions store context information
on g
which is the application context. Prior to this many extensions (including
Flask-Security and Flask-Login) stored things like user credential information on the
request context. These are now stored on g
i.e. the application context. It is imperative
that applications not mistakenly push their own application context and forget to pop it - in that
case Flask won’t push a new application context nor will it pop it at the end of the request - thus
credential information could leak from one user request to another.
Password Validation and Complexity¶
There is a large body of references (and endless discussions) around how to get users to create good passwords. The OWASP Authenication cheatsheet is a useful place to start. Flask-Security has a default password validator that:
Checks for minimum and maximum length (minimum is configurable via
SECURITY_PASSWORD_LENGTH_MIN
). The default is 8 characters as defined by NIST.If
SECURITY_PASSWORD_CHECK_BREACHED
is set, will use the API for haveibeenpwned to check if the password is on a list of breached passwords. The configuration variableSECURITY_PASSWORD_BREACHED_COUNT
can be used to set the minimum allowable ‘breaches’.If
SECURITY_PASSWORD_COMPLEXITY_CHECKER
is set tozxcvbn
and the package zxcvbn is installed, it will check the password for complexity.
Be aware that zxcvbn
is not actively being maintained, and has localization issues.
In addition to validation, unicode passwords should be normalized as specified
by NIST requirement: 5.1.1.2 Memorized Secret Verifiers. Normalization can
be disabled by setting the SECURITY_PASSWORD_NORMALIZE_FORM
to None
.
Validation and normalization is encapsulated in PasswordUtil
.
This can be overridden by passing your class at app initialization time.
The PasswordUtil.validate()
is passed additional kwargs to allow custom
validators more flexibility.
A custom validator can still call the underlying methods where appropriate:
flask_security.password_length_validator()
, flask_security.password_complexity_validator()
,
and flask_security.password_breached_validator()
.
Generic Responses - Avoiding User Enumeration¶
How an application responds to API requests that contain identity or authentication information
can give would-be attackers insight into active users on the system. OWASP has a great cheat-sheet describing
this and useful ways to avoid it. Flask-Security supports this by setting the
SECURITY_RETURN_GENERIC_RESPONSES
configuration to True
. As documented in the cheat-sheet - this does
come with some usability concerns. The following endpoints are affected:
SECURITY_REGISTER_URL
- The same response will be returned whether the email (or username) is already in the system or not. JSON requests will ALWAYS return 200. IfSECURITY_CONFIRMABLE
is set (it should be!), the SECURITY_MSG_CONFIRM_REGISTRATION message will be flashed for both new and existing email addresses. Detailed errors will still be returned for things like insufficient password complexity, etc.. In the case of trying to register an existing email, an email will be sent to that email address explaining that they are already registered and displaying the associated username (if any) and provide a hint on how to reset their password if they forgot it. In the case of a new email but an already registered username, an email will be sent saying that the user must try registering again with a different username.
SECURITY_LOGIN_URL
- For any errors (unknown username, inactive account, bad password) the SECURITY_MSG_GENERIC_AUTHN_FAILED message will be returned.
SECURITY_RESET_URL
- In all cases the SECURITY_MSG_PASSWORD_RESET_REQUEST message will be flashed. For JSON a 200 will always be returned (whether an email was sent or not).
SECURITY_CONFIRM_URL
- In all cases the SECURITY_MSG_CONFIRMATION_REQUEST message will be flashed. For JSON a 200 will always be returned (whether an email was sent or not).
SECURITY_US_SIGNIN_SEND_CODE_URL
- The SECURITY_MSG_GENERIC_US_SIGNIN message will be flashed in all cases - whether a selected method is setup for the user or not.
SECURITY_US_SIGNIN_URL
- For any errors (unknown username, inactive account, bad passcode) the SECURITY_MSG_GENERIC_AUTHN_FAILED message will be returned.
SECURITY_US_VERIFY_LINK_URL
- For any errors (unknown username, inactive account, bad passcode) the SECURITY_MSG_GENERIC_AUTHN_FAILED message will be returned.
In the case of an application using a username
as an identity it should be noted that it is possible for a bad-actor to enumerate usernames, albeit slowly,
by parsing emails.
Note also that SECURITY_REQUIRES_CONFIRMATION_ERROR_VIEW
is ignored in these cases. If your application is using WebAuthn, be sure
to set SECURITY_WAN_ALLOW_USER_HINTS
to False
.
CSRF¶
By default, Flask-Security, via Flask-WTForms protects all form based POSTS from CSRF attacks using well vetted per-session hidden-form-field csrf-tokens.
Any web application that relies on session cookies for authentication must have CSRF protection. For more details please read this OWASP CSRF cheatsheet. A couple important take-aways - first - it isn’t about forms versus JSON - it is about how the API is authenticated (session cookies versus authentication token). Second there is the concern about ‘login CSRF’ - is protection needed prior to authentication (yes if you have a really secure/popular site).
Flask-Security strives to support various options for both its endpoints (e.g. /login
)
and the application endpoints (protected with Flask-Security decorators such as auth_required()
).
If your application just uses forms that are derived from Flask-WTF::Flaskform
- you are done.
CSRF: Single-Page-Applications and AJAX/XHR¶
If you are thinking about using authentication tokens in your browser-based UI - read this article on how and where to store authentication tokens. While the article is talking about JWT it applies to Flask-Security tokens as well.
In general, it is considered more secure (and easier) to use sessions for browser based UI, and tokens for service to service and scripts.
For SPA, and especially those that aren’t served via your flask application, there are difficulties with actually retrieving and using a CSRF token. There are 2 normal ways to do this:
Have the csrf-token available via a JSON GET request that can be attached as a header in every mutating request.
Have a cookie that can be read via javascript whose value is the csrf-token that can be attached as a header in every mutating request.
Flask-Security supports both solutions.
Explicit fetch and send of csrf-token¶
The current session CSRF token
is returned on every JSON GET request (to a Flask-Security endpoint) as response['csrf_token`]
.
For web applications that ARE served via flask, it is even easier to get the csrf-token -
https://flask-wtf.readthedocs.io/en/1.0.x/csrf/ gives some useful tips.
Armed with the csrf-token, the UI must include that in every mutating operation. Be careful NOT to include the csrf-token in non-mutating requests (such as GETs). If your application uses GET to actually modify state - please stop.
An example using axios
# This will fetch the csrf-token. Note that we do a GET on the login endpoint
# which will get us the csrf-token even though we aren't yet logged in.
# Note further the 'data: null' and explicit Content-Type header - these are
# critical, otherwise Flask-Security will return the login form.
axios.get('/login',{data: null, headers: {'Content-Type': 'application/json'}}).then(function (resp) {
csrf_token = resp.data['response']['csrf_token']
})
# This will add the token header to each outgoing mutating request.
axios.interceptors.request.use(function (config) {
if (["post", "delete", "patch", "put"].includes(config["method"])) {
if (csrf_token !== '') {
config.headers["X-CSRF-Token"] = csrf_token
}
}
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
Note that we use the header name X-CSRF-Token
as that is one of the default
headers configured in Flask-WTF (WTF_CSRF_HEADERS)
To protect your application’s endpoints (that presumably are not using Flask forms), you need to enable CSRF as described in the FlaskWTF documentation:
flask_wtf.CSRFProtect(app)
This will turn on CSRF protection on ALL endpoints, including Flask-Security. This protection differs slightly from the default that is part of FlaskForm in that it will first look at the request body and see if it can find a form field that contains the csrf-token, and if it can’t, it will check if the request has a header that is listed in WTF_CSRF_HEADERS and use that. Be aware that if you enable this it will ONLY work if you send the session cookie on each request.
Note
It is IMPORTANT that you initialize/call CSRFProtect
PRIOR to initializing Flask_Security.
CSRF: Enable protection for session auth, but not token auth¶
As mentioned above, CSRF is critical for any mutating operation where the authentication credentials are ‘invisibly’ sent - such as a session cookie -
from a browser. But if your endpoint a) can only be authenticated with an attached token or b) can be called either via session OR token;
it is often desirable not to force token API users to deal with CSRF. To solve this, we need to keep CSRFProtect from checking the csrf-token early in the
request and instead defer that decision to later decorators/code. Flask-Security’s authentication decorators (auth_required()
,
auth_token_required()
, and http_auth_required()
all support calling csrf protection based on configuration:
# Disable pre-request CSRF
app.config[WTF_CSRF_CHECK_DEFAULT] = False
# Check csrf for session and http auth (but not token)
app.config[SECURITY_CSRF_PROTECT_MECHANISMS] = ["session", "basic"]
# Enable CSRF protection
flask_wtf.CSRFProtect(app)
@app.route("/")
@auth_required("token", "session")
def home_page():
With this configuration, CSRF won’t be required if the caller uses an authentication token, but if it uses the session cookie it will.
CSRF: Pro-Tips¶
Be aware that for CSRF to work, callers MUST send the session cookie. So for pure API (token based), and no session cookie - there is no way to support ‘login CSRF’. So your app must set
SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS
(or clients must use CSRF/session cookie for logging in then once they have an authentication token, no further need for cookie).If you enable CSRFProtect(app) and you want to support non-form based JSON requests, then you must include the CSRF token in the header (e.g. X-CSRF-Token)
You must enable CSRFProtect(app) if you want to accept the CSRF token in the request header.
Annotate each of your endpoints with a @auth_required decorator (and don’t rely on just a @role_required or @login_required decorator) so that Flask-Security gets control at the appropriate place.
If you can’t use a decorator, Flask-Security exposes the underlying method
flask_security.handle_csrf()
.Consider starting by setting
SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS
to True. Your application likely doesn’t need ‘login CSRF’ protection, and it is frustrating to not even be able to login via API!If you have unauthenticated endpoints that you want to protect with CSRF then use the
flask_security.unauth_csrf()
decorator.