Skip to content

Commit 81a99ea

Browse files
authored
Replace bleach with nh3 for sanitization (#1252)
closes #1103
2 parents 92b3019 + ecf99cb commit 81a99ea

8 files changed

Lines changed: 23 additions & 18 deletions

File tree

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Docs and Chores
3232
+++++++++++++++
3333
- (:issue:`1208`) Remove support for Pony ORM
3434
- (:pr:`1240`) Remove deprecated get_token_status() and convert uses to check_and_get_token_status()
35+
- Replace ``bleach`` (deprecated/unmaintained) with ``nh3`` for username sanitization
3536

3637
Backwards Compatibility Concerns
3738
+++++++++++++++++++++++++++++++++
@@ -53,6 +54,9 @@ Backwards Compatibility Concerns
5354
- With the addition of email template path configuration variables, the
5455
:py:meth:`flask_security.send_mail` method's ``template`` parameter now requires a
5556
complete template path (relative to the application or blueprint root).
57+
- Username sanitization now uses ``nh3`` instead of ``bleach``. If you enabled
58+
:py:data:`SECURITY_USERNAME_ENABLE` you must now install ``nh3`` (included in the
59+
``common`` extra) rather than ``bleach``.
5660

5761
Notes
5862
+++++

docs/configuration.rst

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,12 @@ These configuration keys are used globally across all features.
357357

358358
.. danger::
359359
Make sure your mapper methods guard against malicious user input. For example,
360-
if you allow ``username`` as an identity method you could use `bleach`_::
360+
if you allow ``username`` as an identity method you could use `nh3`_::
361361

362362
def uia_username_mapper(identity):
363-
# we allow pretty much anything - but we bleach it.
364-
return bleach.clean(identity, strip=True)
363+
# we allow pretty much anything - but we sanitize it.
364+
# tags=set() strips all HTML tags rather than allowing nh3's defaults.
365+
return nh3.clean(identity, tags=set())
365366

366367
Default::
367368

@@ -383,7 +384,7 @@ These configuration keys are used globally across all features.
383384
.. versionchanged:: 4.0.0
384385
Changed from list to list of dict.
385386

386-
.. _bleach: https://pypi.org/project/bleach/
387+
.. _nh3: https://pypi.org/project/nh3/
387388

388389
.. py:data:: SECURITY_USER_IDENTITY_MAPPINGS
389390
@@ -987,8 +988,8 @@ Registerable
987988

988989
Validation and normalization is encapsulated in :class:`.UsernameUtil`.
989990
Note that the default validation restricts username input to be unicode
990-
letters and numbers. It also uses ``bleach`` to scrub any risky input. Be
991-
sure your application requirements includes `bleach`_.
991+
letters and numbers. It also uses ``nh3`` to scrub any risky input. Be
992+
sure your application requirements includes `nh3`_.
992993

993994
Default: ``False``
994995

docs/installation.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Supported extras are:
2828

2929
* ``babel`` - Translation services. It will install babel and Flask-Babel.
3030
* ``fsqla`` - Use flask-sqlalchemy and sqlalchemy as your storage interface.
31-
* ``common`` - Install Flask-Mail, argon2 (the default password hash), bcrypt (old commonly used password hash) and bleach.
31+
* ``common`` - Install Flask-Mail, argon2 (the default password hash), bcrypt (old commonly used password hash) and nh3.
3232
* ``mfa`` - Install packages used for multi-factor (two-factor, unified signin, WebAuthn):
3333
cryptography, qrcode, phonenumberslite (note that for SMS you still need
3434
to pick an SMS provider and install appropriate packages), and webauthn.

flask_security/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1992,7 +1992,7 @@ def init_app(
19921992
self._check_modules("webauthn", "WEBAUTHN")
19931993

19941994
if cv("USERNAME_ENABLE", app=app) and self._username_util_cls == UsernameUtil:
1995-
self._check_modules("bleach", "USERNAME_ENABLE")
1995+
self._check_modules("nh3", "USERNAME_ENABLE")
19961996

19971997
# Register so other packages can reference our translations.
19981998
app.jinja_env.globals["_fsdomain"] = self.i18n_domain.gettext

flask_security/username_util.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,17 @@ def check_username(self, username: str) -> str | None:
5656

5757
def normalize(self, username: str) -> str:
5858
"""
59-
Given an input username - return a clean (using bleach) and normalized
59+
Given an input username - return a clean (using nh3) and normalized
6060
(using Python's unicodedata.normalize()) version.
6161
Must be called in app context and uses
6262
:py:data:`SECURITY_USERNAME_NORMALIZE_FORM` config variable.
6363
"""
64-
import bleach
64+
import nh3
6565

6666
if not username:
6767
return ""
6868

69-
username = bleach.clean(username.strip(), strip=True)
69+
username = nh3.clean(username.strip(), tags=set())
7070
if not username:
7171
return ""
7272
cf = cv("USERNAME_NORMALIZE_FORM")
@@ -87,11 +87,11 @@ def validate(self, username: str) -> tuple[str | None, str | None]:
8787
It is important that None be returned if data is an empty string since
8888
otherwise DBs will complain since the field is unique/nullable.
8989
"""
90-
import bleach
90+
import nh3
9191

9292
if not username:
9393
return None, None
94-
uclean = bleach.clean(username.strip(), strip=True)
94+
uclean = nh3.clean(username.strip(), tags=set())
9595
if uclean != username:
9696
return get_message("USERNAME_ILLEGAL_CHARACTERS")[0], None
9797

pyproject-too.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ dependencies = [
4444
[project.optional-dependencies]
4545
babel = ["babel>=2.17.0", "flask_babel>=4.0.0", "humanize>=4.12.0"]
4646
fsqla = ["flask_sqlalchemy>=3.1.1", "sqlalchemy>=2.0.41"]
47-
common = ["argon2_cffi>=25.1.0", "bcrypt>=4.2.1", "flask_mail>=0.10.0", "bleach>=6.0.0"]
47+
common = ["argon2_cffi>=25.1.0", "bcrypt>=4.2.1", "flask_mail>=0.10.0", "nh3>=0.2.15"]
4848
mfa = ["cryptography>=45.0.7", "qrcode>=7.4.2", "phonenumberslite>=8.13.11", "webauthn>=2.6.0"]
4949
low = [
5050
# Lowest supported versions
@@ -60,7 +60,7 @@ low = [
6060
"authlib==1.2.0",
6161
"babel==2.16.0",
6262
"bcrypt==4.0.1",
63-
"bleach==6.0.0",
63+
"nh3==0.2.15",
6464
"freezegun",
6565
"humanize",
6666
"jinja2==3.1.2",

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ dependencies = [
4444
[project.optional-dependencies]
4545
babel = ["babel>=2.17.0", "flask_babel>=4.0.0", "humanize>=4.12.0"]
4646
fsqla = ["flask_sqlalchemy>=3.1.1", "sqlalchemy>=2.0.41"]
47-
common = ["argon2_cffi>=25.1.0", "bcrypt>=4.2.1", "flask_mail>=0.10.0", "bleach>=6.0.0"]
47+
common = ["argon2_cffi>=25.1.0", "bcrypt>=4.2.1", "flask_mail>=0.10.0", "nh3>=0.2.15"]
4848
mfa = ["cryptography>=45.0.7", "qrcode>=7.4.2", "phonenumberslite>=8.13.11", "webauthn>=2.6.0"]
4949
low = [
5050
# Lowest supported versions
@@ -60,7 +60,7 @@ low = [
6060
"authlib==1.2.0",
6161
"babel==2.16.0",
6262
"bcrypt==4.0.1",
63-
"bleach==6.0.0",
63+
"nh3==0.2.15",
6464
"freezegun",
6565
"humanize",
6666
"jinja2==3.1.2",

requirements/tests.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ Flask-SQLAlchemy-Lite
66
argon2-cffi
77
authlib
88
bcrypt
9-
bleach
109
cbor2
1110
coverage
1211
cryptography
@@ -16,6 +15,7 @@ humanize
1615
mongoengine
1716
mongomock
1817
msgcheck
18+
nh3
1919
peewee
2020
phonenumberslite
2121
pydocstyle

0 commit comments

Comments
 (0)