T09 · Insecure Skill Coding Practices
Warning
- Location
- auth.md:58
- Finding
- Timing-Based Account Enumeration in Custom Authentication Backend<![CDATA[ ## Vulnerability Details **File Location**: `auth.md:58-69` **Vulnerability Type**: Authentication timing side channel **Risk Level**: Medium ### Vulnerable Code ```python class EmailBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): user = User.objects.filter(email__iexact=username).first() if user and user.check_password(password) and self.user_can_authenticate(user): return user return None ``` The guidance immediately following the example recognizes the missing protection: ```text Run `check_password` even when the user is absent, or response timing tells an attacker which emails exist. Django's own `ModelBackend` calls `UserModel().set_password(password)` for exactly this reason. ``` ### Technical Analysis The conditional uses Python short-circuit evaluation. If no matching user exists, `user` is false and `user.check_password(password)` is never executed. For an existing account, Django performs an intentionally expensive password-hashing operation. Consequently, authentication attempts for nonexistent accounts are generally faster than attempts for existing accounts with incorrect passwords. Repeated measurements can reduce ordinary network noise and allow an attacker to statistically distinguish registered email addresses. The example contradicts its own subsequent recommendation and may be copied directly into a production application. The issue does not reveal passwords or grant authentication by itself, but it exposes an account-existence oracle. ### Attack Path 1. An attacker obtains or generates a list of candidate email addresses. 2. The attacker submits repeated login attempts using the same invalid password for each candidate. 3. Requests for nonexistent users return without running the password hasher. 4. Requests for existing users invoke `check_password()` and exhibit a measurably higher processing time. 5. The attacker aggregates multi ...[truncated 623 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Replace the short-circuit implementation with one that always performs a password-hashing operation: ```python class EmailBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): UserModel = get_user_model() try: user = UserModel.objects.get(email__iexact=username) except UserModel.DoesNotExist: # Consume approximately the same hashing cost as a real password check. UserModel().set_password(password) return None if user.check_password(password) and self.user_can_authenticate(user): return user return None ``` Additional hardening measures: 1. Add request-level or proxy-level login throttling. 2. Return identical response bodies and status codes for unknown users and incorrect passwords. 3. Add automated tests that verify both paths execute a password-hashing operation. 4. Avoid strict single-request timing assertions because timing is noisy; instead, test the control flow through mocked hasher calls. 5. Monitor distributed authentication failures without logging submitted credentials. ]]>
