Opened 2 months ago
Last modified 2 days ago
#36901 assigned Cleanup/optimization
Centralize mitigations against timing attacks targeting user enumeration
| Reported by: | Jacob Walls | Owned by: | Afenomamy |
|---|---|---|---|
| Component: | contrib.auth | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Jake Howard | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | yes |
| Needs tests: | yes | Patch needs improvement: | yes |
| Easy pickings: | no | UI/UX: | no |
Description
The patch for CVE 2025-13473 (3eb814e02a4c336866d4189fa0c24fd1875863ed) runs the default password hasher once in django.contrib.auth.handlers.modwsgi just like ModelBackend has done since #20760.
A refactor in exposing this functionality in a central place that the mod_wsgi auth handler could just call is worth exploring.
The Security Team decided against attempting that refactoring in a patch release.
Additional findings:
aauthenticate()still does manual extra hashingverify_password()still does manual extra hashing- in the
mod_wsgiauth handler, handling of custom user models lacking the "is_active" attribute should be clarified. It currently raisesAttributeError, but to align withModelBackend, we could change the semantic.
So, the acceptance requirements for this ticket would be something like:
- no behavior changes
- except for possibly the
AttributeErroredge case discussed above, if documented appropriately - reduce the number of calls to
set_password("") - avoid coupling the
modwsgihandler toModelBackend
Another outcome would be a bit of work showing that this has pitfalls or isn't worth it.
Change History (13)
comment:1 by , 2 months ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:2 by , 2 months ago
| Summary: | Centralize mitigations against timing attacks → Centralize mitigations against timing attacks targeting user enumeration |
|---|
comment:4 by , 3 weeks ago
Hello, this is Aina (Team Saturn) from Djangonaut Space. I will be looking at this ticket
comment:5 by , 3 weeks ago
| Owner: | changed from to |
|---|
comment:6 by , 2 weeks ago
So my plan is :
1 - Create two utility functions called get_user_with_mitigation and aget_user_with_mitigation inside django.contrib.auth.init.py.
2 - Update django.contrib.auth.handlers.modwsgi.check_password to use get_user_with_mitigation
3 - Refactor authenticate and aauthenticate in django.contrib.auth.backends.py to use get_user_with_mitigation and aget_user_with_mitigation .
NB :
- For Attribute Safety: getattr(user, 'is_active', True) will be used to ensure compatibility with custom user models that do not define an is_active field. This will resolve the reported AttributeError.
- For Performance: make_password will be called directly for the dummy hashing. This avoids the overhead of instantiating a UserModel instance simply to call set_password().
- I think adding more tests is not needed.
comment:7 by , 2 weeks ago
| Has patch: | set |
|---|
comment:8 by , 2 weeks ago
@Jacob Walls, can you clarify what you mean by "reduce the number of calls to set_password("")" please? Is it to remove the calls because that is a hack and we should have a clearer method or to reduce the amount of times the hashing mechanism is run?
comment:9 by , 2 weeks ago
Certainly. The Security Team was concerned that the reason we missed CVE 2025-13473 -- the bug where Django wasn't running the password hasher once for inactive users in the modwsgi handler -- was because of a DRY (Don't Repeat Yourself) problem. Since there was no central utility encapsulating "get active user and check password, or else run password hasher once".
That's what I meant here:
A refactor in exposing this functionality in a central place that the mod_wsgi auth handler could just call is worth exploring.
Could we refactor both ModelBackend and modwsgi handler (and possibly any other places) to just call one utility encapsulating the security concept, in case we need tweaks to it later.
comment:10 by , 2 weeks ago
Ah, I think I see what you may have been asking. Let's say there are four calls to set_password(""). I'm suggesting that there should be four calls to a helper that manages that instead, since calling set_password("") in the right conditions is tricky to get right.
comment:11 by , 2 weeks ago
That is more of what I was looking for, but your earlier answer was helpful too. Thank you.
comment:12 by , 2 weeks ago
| Patch needs improvement: | set |
|---|
comment:13 by , 5 days ago
| Needs documentation: | set |
|---|---|
| Needs tests: | set |
Tentatively passing over to Sarah to evaluate if a good fit for the next Djangonaut Space session. Can return to the pool if not.