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.
Freshness¶
A common pattern for browser-based sites is to use sessions to manage identity. This is usually implemented using session cookies. These cookies expire once the session (browser tab) is closed. This is very convenient, and keeps the users from having to constantly re-authenticate. The downside is that sessions can easily be open for days or weeks. This adds to the security risk that some bad-actor or XSS gets control of the browser and then can do anything the user can. To mitigate that, operations that change fundamental identity characteristics (such as email, password, etc.) can be protected by requiring a ‘fresh’ or recent authentication. Flask-Security supports this with the following:
auth_required()
takes parameters that define how recent the authentication must have happened. In addition a grace period can be specified so that multiple step operations don’t require re-authentication in the middle.A default
Security.reauthn_handler()
that is called when a request fails the recent authentication check.
SECURITY_VERIFY_URL
,SECURITY_US_VERIFY_URL
,SECURITY_WAN_VERIFY_URL
endpoints that request the user to re-authenticate.
VerifyForm
,UsVerifyForm
,WebAuthnVerifyForm
forms that can be extended.
Flask-Security itself uses this as part of securing the following endpoints:
.wan_register (“/wan-register”)
.wan_delete (“/wan-delete”)
.tf_setup (“/tf-setup”)
.us_setup (“/us-setup”)
.mf_recovery_codes (“/mf-recovery-codes”)
Using the SECURITY_FRESHNESS
and SECURITY_FRESHNESS_GRACE_PERIOD
configuration variables.
Tip
Freshness requires a session (cookie) be sent as part of the request. Without
a session, freshness will fail. If your application doesn’t/can’t send session cookies
you can disable freshness by setting SECURITY_FRESHNESS
to timedelta(minutes=-1)
Open Redirect Exposure¶
Flask-Security, accepts a next=xx
parameter (either
as a query param OR in the POSTed form) which it will use when completing an operation
which results in a redirection. If a malicious user/
application can inject an arbitrary next
parameter which redirects to an external
location, this results in a security vulnerability called an open redirect.
The following endpoints accept a next
parameter:
.login (“/login”)
.logout (“/logout”)
.register (“/register”)
.verify (“/verify”)
.two_factor_token_validation (“/tf-validate”)
.wan_verify_response (“/wan-verify”)
.wan_signin_response (“/wan-signin”)
.us_signin (“/us-signin”)
.us_verify (“/us-verify”)
Flask-Security always quotes the path portion of a user supplied URL. This link provides background of why simple parsing of URLs isn’t enough.
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 Authentication 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).Note
: If the application overrides the form and adds an additional field (e.g. captcha) and that field has a validation error, a normal form error response will be returned (and JSON will return a 400).
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).Note
: If the application overrides the form and adds an additional field (e.g. captcha) and that field has a validation error, a normal form error response will be returned (and JSON will return a 400).
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.
Note that all of Flask-Security’s endpoints are form based (regardless of how the request was made).
Behind-The-Scenes¶
- Depending on configuration, there are 3 places CSRF tokens can be checked:
As part of form validation for any form derived from FlaskForm (which all Flask-Security forms are. An error here is recorded in the
csrf_token
field and the calling view decided whether to return 200 or 400 (all Flask-Security views return 200 with form field errors). This is the default if no other configuration changes are made.As part of an @before_request handler that Flask-WTF sets up if CSRFprotect() is called. On error this always returns HTTP 400 and small snippet of HTML. This can be disabled by setting config[“WTF_CSRF_CHECK_DEFAULT”] = True.
As part of a Flask-Security decorator (
unauth_csrf()
,auth_required()
). On error either a JSON response is returned OR CSRFError exception is raised and 400 is returned with the small snippet of HTML (the exception and default response is part of Flask-WTF).
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.2.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.
Note
Calling CSRFProtect(app) will setup a @before_request handler to verify CSRF - this occurs BEFORE any Flask-Security decorators or other view/form logic. One side effect is that CSRFProtect, on error, will raise a BadRequest error which returns a small piece of HTML by default - your application will need to add a Flask ErrorHandler to change that. Alternatively, and recommended is to set WTF_CSRF_CHECK_DEFAULT to False - which will disable the @before_request and let Flask-Security handle CSRF protection including properly returning a JSON response if the caller asks for it.
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 send request data as JSON, 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.