Skip to content

Commit bf353fd

Browse files
authored
Simple check for null user_id on load. (#1122)
If properly configured this should never happen - but for folks upgrading from old Flask-Security its possible. We shouldn't ever return a DB record with a null fs_uniquifier. close #1116
1 parent dda5df6 commit bf353fd

File tree

5 files changed

+60
-3
lines changed

5 files changed

+60
-3
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default_language_version:
44
python: python3.12
55
repos:
66
- repo: https://github.com/pre-commit/pre-commit-hooks
7-
rev: v5.0.0
7+
rev: v6.0.0
88
hooks:
99
- id: trailing-whitespace
1010
- id: debug-statements
@@ -20,7 +20,7 @@ repos:
2020
- id: pyupgrade
2121
args: [--py310-plus]
2222
- repo: https://github.com/psf/black
23-
rev: 25.1.0
23+
rev: 25.9.0
2424
hooks:
2525
- id: black
2626
- repo: https://github.com/pycqa/flake8

flask_security/core.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,12 @@ class FormInfo:
693693

694694

695695
def _user_loader(user_id):
696-
"""Load based on fs_uniquifier (alternative_id)."""
696+
"""Load based on fs_uniquifier (alternative_id).
697+
If the db model and db are properly configured and set there is no way we should
698+
ever see a null user_id. But it is clearly wrong.
699+
"""
700+
if not user_id:
701+
return None
697702
user = _security.datastore.find_user(fs_uniquifier=str(user_id))
698703
if user and user.active:
699704
set_request_attr("fs_authn_via", "session")

tests/test_datastore.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,3 +771,18 @@ class User(Model, sqla.FsUserMixin):
771771
with app.app_context():
772772
Model.metadata.drop_all(db.engine)
773773
db.engine.dispose()
774+
775+
776+
def test_null_fs_uniquifier(app, client):
777+
# If a record has a null fs_uniquifier - we shouldn't find it.
778+
# The only way this might happen is if an app upgrades from a 3.0 Flask-Security
779+
# and doesn't properly update their DB.
780+
ds = app.security.datastore
781+
with app.test_request_context("/"):
782+
user = ds.find_user(email="gal@lp.com")
783+
user.fs_uniquifier = ""
784+
ds.put(user)
785+
ds.commit()
786+
787+
user = app.security.login_manager.user_callback("")
788+
assert not user

tests/test_misc.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
populate_data,
3636
reset_fresh,
3737
get_form_input,
38+
is_authenticated,
3839
)
3940
from tests.test_webauthn import HackWebauthnUtil, reg_2_keys
4041

@@ -1599,3 +1600,13 @@ def test_password_required_setting(app, sqlalchemy_datastore):
15991600
with pytest.raises(ValueError) as vex:
16001601
Security(app=app, datastore=sqlalchemy_datastore)
16011602
assert "SECURITY_PASSWORD_REQUIRED can only be" in str(vex.value)
1603+
1604+
1605+
def test_null_user_id(app, client, get_message):
1606+
# if DB not configured correctly - make sure we catch null fs_uniquifier
1607+
json_authenticate(client)
1608+
assert is_authenticated(client, get_message)
1609+
with client.session_transaction() as sess:
1610+
sess["_user_id"] = ""
1611+
sess["user_id"] = ""
1612+
assert not is_authenticated(client, get_message)

tests/test_registerable.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
init_app_with_options,
1818
json_authenticate,
1919
logout,
20+
is_authenticated,
2021
)
2122

2223
from flask_security import Security, UserMixin, user_registered, user_not_registered
@@ -578,13 +579,15 @@ def test_username(app, clients, get_message):
578579
"/login", json=dict(username="dude", password="awesome sunset")
579580
)
580581
assert response.status_code == 200
582+
assert is_authenticated(client, get_message)
581583
logout(client)
582584

583585
# login with email
584586
response = client.post(
585587
"/login", json=dict(email="dude@lp.com", password="awesome sunset")
586588
)
587589
assert response.status_code == 200
590+
assert is_authenticated(client, get_message)
588591
logout(client)
589592

590593
response = client.post(
@@ -621,6 +624,29 @@ def test_username(app, clients, get_message):
621624
)
622625

623626

627+
@pytest.mark.settings(username_enable=True)
628+
def test_username_not_set(app, client, get_message):
629+
# login with null username - shouldn't match
630+
# note that this is caught at LoginForm - it doesn't force a DB call
631+
response = client.post(
632+
"/register",
633+
json=dict(
634+
email="justemail@lp.com",
635+
username="",
636+
password="awesome sunset",
637+
password_confirm="awesome sunset",
638+
),
639+
)
640+
assert response.status_code == 200
641+
client.post("/logout")
642+
response = client.post("/login", json=dict(username="", password="awesome sunset"))
643+
assert response.status_code == 400
644+
assert (
645+
get_message("USER_DOES_NOT_EXIST")
646+
== response.json["response"]["field_errors"][""][0].encode()
647+
)
648+
649+
624650
@pytest.mark.settings(username_enable=True)
625651
def test_username_template(app, client):
626652
# verify template displays username option

0 commit comments

Comments
 (0)