Skip to content

Commit 0bf4f8e

Browse files
authored
Add support for generic response messages. (pallets-eco#632)
The new SECURITY_RETURN_GENERIC_RESPONSES flag, if set to True will make Flask-Security consistent with the OWASP Authentication errors cheat-sheet recommendations. This affects the /register, /login, /forgot, /confirm, /us-signin, /verify endpoints. This adds new signal (user_not_registered), new email templates (welcome_existing, welcome_existing_username) to improve UX in the case of someone trying to register an already registered email/username. close pallets-eco#585
1 parent fc6401d commit 0bf4f8e

26 files changed

+860
-95
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ repos:
1414
- id: check-merge-conflict
1515
- id: fix-byte-order-marker
1616
- repo: https://github.com/asottile/pyupgrade
17-
rev: v2.34.0
17+
rev: v2.37.1
1818
hooks:
1919
- id: pyupgrade
2020
args: [--py37-plus]
2121
- repo: https://github.com/psf/black
22-
rev: 22.3.0
22+
rev: 22.6.0
2323
hooks:
2424
- id: black
2525
- repo: https://gitlab.com/pycqa/flake8

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Features
2424
Fixes
2525
+++++
2626
- (:pr:`591`) Make the required zxcvbn complexity score configurable (mephi42)
27+
- (:issue:`585`) Provide option to prevent user enumeration.
2728
- (:issue:`531`) Get rid of Flask-Mail. Flask-Mailman is now the default preferred email package.
2829
Flask-Mail is still supported so there should be no backwards compatability issues.
2930
- (:issue:`597`) A delete option has been added to us-setup (form and view).
@@ -60,6 +61,8 @@ For unified signin:
6061
- ``SECURITY_US_VERIFY_SEND_CODE_URL`` and ``SECURITY_US_SIGNIN_SEND_CODE_URL`` endpoints are now POST only.
6162
- Empty passwords were always permitted when ``SECURITY_UNIFIED_SIGNIN`` was enabled - now an additional configuration
6263
variable ``SECURITY_PASSWORD_REQUIRED`` must be set to False.
64+
- ``SECURITY_US_VERIFY_SEND_CODE_URL`` and ``SECURITY_US_SIGNIN_SEND_CODE_URL`` used to send ``code_sent`` to the template.
65+
Now they flash the ``SECURITY_MSG_CODE_HAS_BEEN_SENT`` message.
6366

6467
Login:
6568

docs/api.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,19 @@ sends the following signals.
268268
`form_data` is a dictionary representation of registration form's content
269269
received with the registration request.
270270

271+
.. data:: user_not_registered
272+
273+
Sent when a user attempts to register, but is already registered. This is ONLY sent
274+
when :py:data:`SECURITY_RETURN_GENERIC_RESPONSES` is enabled. It is passed the
275+
following arguments:
276+
277+
* `user` - The existing user model
278+
* `existing_email` - True if attempting to register an existing email
279+
* `existing_username`- True if attempting to register an existing username
280+
* `form_data` - the entire contents of the posted request form
281+
282+
.. versionadded:: 5.0.0
283+
271284
.. data:: user_confirmed
272285

273286
Sent when a user is confirmed. In addition to the app (which is the

docs/configuration.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,13 @@ These configuration keys are used globally across all features.
435435

436436
Default: ``False``.
437437

438+
.. py:data:: SECURITY_RETURN_GENERIC_RESPONSES
439+
440+
If set to ``True`` Flask-Security will return generic responses to endpoints
441+
that could be used to enumerate users. Please see :ref:`generic_responses`.
442+
443+
.. versionadded:: 5.0.0
444+
438445
.. py:data:: SECURITY_BACKWARDS_COMPAT_UNAUTHN
439446
440447
If set to ``True`` then the default behavior for authentication
@@ -1515,7 +1522,7 @@ Recovery Codes
15151522
To enable this feature - set this to ``True``. Please see :ref:`models_topic` for
15161523
required additions to your database models. This enables a user to generate and
15171524
use a recovery code for two-factor authentication. This works for all two-factor
1518-
mechanisms - including webauthn. Note that these code are single use and
1525+
mechanisms - including WebAuthn. Note that these code are single use and
15191526
the user should be advised to write them down and store in a safe place.
15201527

15211528
.. py:data:: SECURITY_MULTI_FACTOR_RECOVERY_CODES_N
@@ -1642,6 +1649,7 @@ The default messages and error levels can be found in ``core.py``.
16421649
* ``SECURITY_MSG_ALREADY_CONFIRMED``
16431650
* ``SECURITY_MSG_API_ERROR``
16441651
* ``SECURITY_MSG_ANONYMOUS_USER_REQUIRED``
1652+
* ``SECURITY_MSG_CODE_HAS_BEEN_SENT``
16451653
* ``SECURITY_MSG_CONFIRMATION_EXPIRED``
16461654
* ``SECURITY_MSG_CONFIRMATION_REQUEST``
16471655
* ``SECURITY_MSG_CONFIRMATION_REQUIRED``
@@ -1652,6 +1660,9 @@ The default messages and error levels can be found in ``core.py``.
16521660
* ``SECURITY_MSG_EMAIL_NOT_PROVIDED``
16531661
* ``SECURITY_MSG_FAILED_TO_SEND_CODE``
16541662
* ``SECURITY_MSG_FORGOT_PASSWORD``
1663+
* ``SECURITY_MSG_GENERIC_AUTHN_FAILED``
1664+
* ``SECURITY_MSG_GENERIC_RECOVERY``
1665+
* ``SECURITY_MSG_GENERIC_US_SIGNIN``
16551666
* ``SECURITY_MSG_IDENTITY_ALREADY_ASSOCIATED``
16561667
* ``SECURITY_MSG_INVALID_CODE``
16571668
* ``SECURITY_MSG_INVALID_CONFIRMATION_TOKEN``

docs/customizing.rst

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ The following is a list of email templates:
303303
* `security/email/change_notice.html`
304304
* `security/email/welcome.html`
305305
* `security/email/welcome.txt`
306+
* `security/email/welcome_existing.html`
307+
* `security/email/welcome_existing.txt`
308+
* `security/email/welcome_existing_username.html`
309+
* `security/email/welcome_existing_username.txt`
306310
* `security/email/two_factor_instructions.html`
307311
* `security/email/two_factor_instructions.txt`
308312
* `security/email/two_factor_rescue.html`
@@ -338,33 +342,37 @@ to ``False`` will bypass sending of the email (they all default to ``True``).
338342
In most cases, in addition to an email being sent, a :ref:`Signal <signals_topic>` is sent.
339343
The table below summarizes all this:
340344

341-
============================= ================================ ============================================= ====================== ===============================
342-
**Template Name** **Gate Config** **Subject Config** **Context Vars** **Signal Sent**
343-
----------------------------- -------------------------------- --------------------------------------------- ---------------------- -------------------------------
344-
welcome SECURITY_SEND_REGISTER_EMAIL SECURITY_EMAIL_SUBJECT_REGISTER - user user_registered
345-
- confirmation_link
346-
- confirmation_token
347-
confirmation_instructions N/A SECURITY_EMAIL_SUBJECT_CONFIRM - user confirm_instructions_sent
348-
- confirmation_link
349-
- confirmation_token
350-
login_instructions N/A SECURITY_EMAIL_SUBJECT_PASSWORDLESS - user login_instructions_sent
351-
- login_link
352-
- login_token
353-
reset_instructions SEND_PASSWORD_RESET_EMAIL SECURITY_EMAIL_SUBJECT_PASSWORD_RESET - user reset_password_instructions_sent
354-
- reset_link
355-
- reset_token
356-
reset_notice SEND_PASSWORD_RESET_NOTICE_EMAIL SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE - user password_reset
357-
358-
change_notice SEND_PASSWORD_CHANGE_EMAIL SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE - user password_changed
359-
two_factor_instructions N/A SECURITY_EMAIL_SUBJECT_TWO_FACTOR - user tf_security_token_sent
360-
- token
361-
- username
362-
two_factor_rescue N/A SECURITY_EMAIL_SUBJECT_TWO_FACTOR_RESCUE - user N/A
363-
us_instructions N/A SECURITY_US_EMAIL_SUBJECT - user us_security_token_sent
364-
- login_token
365-
- login_link
366-
- username
367-
============================= ================================ ============================================= ====================== ===============================
345+
============================= ================================== ============================================= ====================== ===============================
346+
**Template Name** **Gate Config** **Subject Config** **Context Vars** **Signal Sent**
347+
----------------------------- ---------------------------------- --------------------------------------------- ---------------------- -------------------------------
348+
welcome SECURITY_SEND_REGISTER_EMAIL SECURITY_EMAIL_SUBJECT_REGISTER - user user_registered
349+
- confirmation_link
350+
- confirmation_token
351+
confirmation_instructions N/A SECURITY_EMAIL_SUBJECT_CONFIRM - user confirm_instructions_sent
352+
- confirmation_link
353+
- confirmation_token
354+
login_instructions N/A SECURITY_EMAIL_SUBJECT_PASSWORDLESS - user login_instructions_sent
355+
- login_link
356+
- login_token
357+
reset_instructions SEND_PASSWORD_RESET_EMAIL SECURITY_EMAIL_SUBJECT_PASSWORD_RESET - user reset_password_instructions_sent
358+
- reset_link
359+
- reset_token
360+
reset_notice SEND_PASSWORD_RESET_NOTICE_EMAIL SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE - user password_reset
361+
362+
change_notice SEND_PASSWORD_CHANGE_EMAIL SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE - user password_changed
363+
two_factor_instructions N/A SECURITY_EMAIL_SUBJECT_TWO_FACTOR - user tf_security_token_sent
364+
- token
365+
- username
366+
two_factor_rescue N/A SECURITY_EMAIL_SUBJECT_TWO_FACTOR_RESCUE - user N/A
367+
us_instructions N/A SECURITY_US_EMAIL_SUBJECT - user us_security_token_sent
368+
- login_token
369+
- login_link
370+
- username
371+
welcome_existing SECURITY_SEND_REGISTER_EMAIL SECURITY_EMAIL_SUBJECT_REGISTER - user user_not_registered
372+
SECURITY_RETURN_GENERIC_RESPONSES - recovery_link
373+
welcome_existing_username SECURITY_SEND_REGISTER_EMAIL SECURITY_EMAIL_SUBJECT_REGISTER - email user_not_registered
374+
SECURITY_RETURN_GENERIC_RESPONSES - username
375+
============================= ================================== ============================================= ====================== ===============================
368376

369377
When sending an email, Flask-Security goes through the following steps:
370378

@@ -375,7 +383,7 @@ When sending an email, Flask-Security goes through the following steps:
375383

376384
#. Calls :meth:`.MailUtil.send_mail` with all the required parameters.
377385

378-
The default implementation of ``MailUtil.send_mail`` uses Flask-Mail to create and send the message.
386+
The default implementation of ``MailUtil.send_mail`` uses flask-mailman to create and send the message.
379387
By providing your own implementation, you can use any available python email handling package.
380388

381389
Email subjects are by default localized - see above section on Localization to learn how

docs/patterns.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,47 @@ and :func:`flask_security.password_breached_validator`.
115115

116116
.. _5.1.1.2 Memorized Secret Verifiers: https://pages.nist.gov/800-63-3/sp800-63b.html#sec5
117117

118+
119+
.. _generic_responses:
120+
121+
Generic Responses - Avoiding User Enumeration
122+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
123+
How an application responds to API requests that contain identity or authentication information
124+
can give would-be attackers insight into active users on the system. OWASP has a great `cheat-sheet`_ describing
125+
this and useful ways to avoid it. Flask-Security supports this by setting the
126+
:py:data:`SECURITY_RETURN_GENERIC_RESPONSES` configuration to ``True``. As documented in the cheat-sheet - this does
127+
come with some usability concerns. The following endpoints are affected:
128+
129+
* :py:data:`SECURITY_REGISTER_URL` - The same response will be returned whether the email (or username) is already in the
130+
system or not. JSON requests will ALWAYS return 200. If :py:data:`SECURITY_CONFIRMABLE` is set (it should be!), the
131+
`SECURITY_MSG_CONFIRM_REGISTRATION` message will be flashed for both new and existing email addresses. Detailed errors will still
132+
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
133+
explaining that they are already registered and displaying the associated username (if any) and provide a hint on how to reset their
134+
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
135+
user must try registering again with a different username.
136+
* :py:data:`SECURITY_LOGIN_URL` - For any errors (unknown username, inactive account, bad password) the `SECURITY_MSG_GENERIC_AUTHN_FAILED`
137+
message will be returned.
138+
* :py:data:`SECURITY_RESET_URL` - In all cases the `SECURITY_MSG_PASSWORD_RESET_REQUEST` message will be flashed. For JSON
139+
a 200 will always be returned (whether an email was sent or not).
140+
* :py:data:`SECURITY_CONFIRM_URL` - In all cases the `SECURITY_MSG_CONFIRMATION_REQUEST` message will be flashed. For JSON
141+
a 200 will always be returned (whether an email was sent or not).
142+
* :py:data:`SECURITY_US_SIGNIN_SEND_CODE_URL` - The `SECURITY_MSG_GENERIC_US_SIGNIN` message will be flashed in all cases -
143+
whether a selected method is setup for the user or not.
144+
* :py:data:`SECURITY_US_SIGNIN_URL` - For any errors (unknown username, inactive account, bad passcode) the `SECURITY_MSG_GENERIC_AUTHN_FAILED`
145+
message will be returned.
146+
* :py:data:`SECURITY_US_VERIFY_LINK_URL` - For any errors (unknown username, inactive account, bad passcode) the `SECURITY_MSG_GENERIC_AUTHN_FAILED`
147+
message will be returned.
148+
149+
150+
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,
151+
by parsing emails.
152+
153+
Note also that :py:data:`SECURITY_REQUIRES_CONFIRMATION_ERROR_VIEW` is ignored in these cases. If your application is using WebAuthn, be sure
154+
to set :py:data:`SECURITY_WAN_ALLOW_USER_HINTS` to ``False``.
155+
156+
157+
.. _cheat-sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-and-error-messages
158+
118159
.. _csrftopic:
119160

120161
CSRF

flask_security/core.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"URL_PREFIX": None,
120120
"SUBDOMAIN": None,
121121
"FLASH_MESSAGES": True,
122+
"RETURN_GENERIC_RESPONSES": False,
122123
"I18N_DOMAIN": "flask_security",
123124
"I18N_DIRNAME": pkg_resources.resource_filename("flask_security", "translations"),
124125
"EMAIL_VALIDATOR_ARGS": None,
@@ -350,6 +351,21 @@
350351
#: Default Flask-Security messages
351352
_default_messages = {
352353
"API_ERROR": (_("Input not appropriate for requested API"), "error"),
354+
"GENERIC_AUTHN_FAILED": (
355+
_("Authentication failed - identity or password/passcode invalid"),
356+
"error",
357+
),
358+
"GENERIC_RECOVERY": (
359+
_(
360+
"If that email address is in our system, "
361+
"you will receive an email describing how to reset your password."
362+
),
363+
"info",
364+
),
365+
"GENERIC_US_SIGNIN": (
366+
_("If that identity is in our system, you were sent a code."),
367+
"info",
368+
),
353369
"UNAUTHORIZED": (_("You do not have permission to view this resource."), "error"),
354370
"UNAUTHENTICATED": (
355371
_("You are not authenticated. Please supply the correct credentials."),
@@ -458,6 +474,7 @@
458474
_("You can only access this endpoint when not logged in."),
459475
"error",
460476
),
477+
"CODE_HAS_BEEN_SENT": (_("Code has been sent."), "info"),
461478
"FAILED_TO_SEND_CODE": (_("Failed to send code. Please try again later"), "error"),
462479
"TWO_FACTOR_INVALID_TOKEN": (_("Invalid code"), "error"),
463480
"TWO_FACTOR_LOGIN_SUCCESSFUL": (_("Your code has been confirmed"), "success"),

0 commit comments

Comments
 (0)