This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 0a130b38b59 Use hmac.compare_digest for SimpleAuthManager password
compare (CWE-208) (#66556)
0a130b38b59 is described below
commit 0a130b38b595f1245f9313fa262821f40d61e70c
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu May 7 19:00:16 2026 +0200
Use hmac.compare_digest for SimpleAuthManager password compare (CWE-208)
(#66556)
The SimpleAuthManager login service compared submitted passwords against
the stored value with `==`, which short-circuits on the first differing
character and leaks password length / prefix information through timing
analysis. Switch to `hmac.compare_digest()` so the comparison runs in
constant time regardless of where the strings diverge, and use
`.get(user.username, "")` so a user record without a corresponding
password entry compares against an empty string rather than raising
`KeyError`.
SimpleAuthManager is dev-only, but the threat model the audit raised
(timing analysis from a co-located process or local network) is plausible
even in dev, and the fix is one line. ASVS L1/L2/L3 ยง 6.3.1 / CWE-208.
Tests added for wrong-password and unknown-username rejection.
Reported by the L3 ASVS sweep at apache/tooling-agents#23 (FINDING-008).
---
.../auth/managers/simple/services/login.py | 11 ++++++++-
.../auth/managers/simple/services/test_login.py | 26 ++++++++++++++++++++++
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/services/login.py
b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/services/login.py
index 8e64861de1a..799336ae0f8 100644
---
a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/services/login.py
+++
b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/services/login.py
@@ -17,6 +17,8 @@
from __future__ import annotations
+import hmac
+
from fastapi import HTTPException, status
from airflow.api_fastapi.app import get_auth_manager
@@ -53,10 +55,17 @@ class SimpleAuthManagerLogin:
users = SimpleAuthManager.get_users()
passwords = SimpleAuthManager.get_passwords()
+ # Use hmac.compare_digest for constant-time password comparison
(CWE-208).
+ # `passwords.get(..., "")` keeps the comparison constant-time against
an
+ # empty string when the user record exists but the password entry is
missing.
found_users = [
user
for user in users
- if user.username == body.username and passwords[user.username] ==
body.password
+ if user.username == body.username
+ and hmac.compare_digest(
+ passwords.get(user.username, "").encode("utf-8"),
+ body.password.encode("utf-8"),
+ )
]
if len(found_users) == 0:
diff --git
a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/services/test_login.py
b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/services/test_login.py
index 3318f803cc7..5b9b4850ff1 100644
---
a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/services/test_login.py
+++
b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/services/test_login.py
@@ -121,3 +121,29 @@ class TestLogin:
with pytest.raises(HTTPException) as ex:
SimpleAuthManagerLogin.create_token_all_admins()
assert ex.value.status_code == 403
+
+ @pytest.mark.db_test
+ def test_create_token_wrong_password_rejected(self, auth_manager):
+ """Wrong password is rejected via constant-time comparison
(CWE-208)."""
+ with conf_vars({("core", "simple_auth_manager_users"):
f"{TEST_USER_1}:{TEST_ROLE_1}"}):
+ auth_manager.init()
+ with pytest.raises(HTTPException) as ex:
+ SimpleAuthManagerLogin.create_token(
+ body=LoginBody(username=TEST_USER_1,
password="wrong-password"),
+ expiration_time_in_seconds=1,
+ )
+ assert ex.value.status_code == 401
+ assert "Invalid credentials" in ex.value.detail
+
+ @pytest.mark.db_test
+ def test_create_token_unknown_user_rejected(self, auth_manager):
+ """Unknown username is rejected without leaking timing info
(CWE-208)."""
+ with conf_vars({("core", "simple_auth_manager_users"):
f"{TEST_USER_1}:{TEST_ROLE_1}"}):
+ auth_manager.init()
+ with pytest.raises(HTTPException) as ex:
+ SimpleAuthManagerLogin.create_token(
+ body=LoginBody(username="nonexistent",
password="any-password"),
+ expiration_time_in_seconds=1,
+ )
+ assert ex.value.status_code == 401
+ assert "Invalid credentials" in ex.value.detail