mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Add: Comprehensive solution summary document
This commit is contained in:
parent
3cb6c99542
commit
4dec1187e2
1 changed files with 206 additions and 0 deletions
206
SOLUTION_SUMMARY.md
Normal file
206
SOLUTION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# Django Password Reset Token Security Fix - Solution Summary
|
||||
|
||||
## Problem Statement
|
||||
A security vulnerability exists in Django's password reset token mechanism. If a user:
|
||||
1. Requests a password reset (generating a token based on their email)
|
||||
2. Changes their email address before using the token
|
||||
3. Uses the original reset token
|
||||
|
||||
The token will still be accepted, even though the email has changed.
|
||||
|
||||
## Root Cause
|
||||
The `PasswordResetTokenGenerator._make_hash_value()` method in `django/contrib/auth/tokens.py` does not include the user's email address in the token hash computation. This means email changes are not detected during token validation.
|
||||
|
||||
## Solution
|
||||
Include the user's email address in the token generation hash so that:
|
||||
- Changing email invalidates existing reset tokens
|
||||
- Token validation is tied to the email at the time of generation
|
||||
- Custom user models are supported via `get_email_field_name()`
|
||||
|
||||
## Implementation
|
||||
|
||||
### File: `django/contrib/auth/tokens.py`
|
||||
|
||||
**Method Modified:** `PasswordResetTokenGenerator._make_hash_value()`
|
||||
|
||||
```python
|
||||
def _make_hash_value(self, user, timestamp):
|
||||
"""
|
||||
Hash the user's primary key and some user state that's sure to change
|
||||
after a password reset to produce a token that invalidated when it's
|
||||
used:
|
||||
1. The password field will change upon a password reset (even if the
|
||||
same password is chosen, due to password salting).
|
||||
2. The last_login field will usually be updated very shortly after
|
||||
a password reset.
|
||||
3. The email field will change if the user changes their email address.
|
||||
Failing those things, settings.PASSWORD_RESET_TIMEOUT eventually
|
||||
invalidates the token.
|
||||
|
||||
Running this data through salted_hmac() prevents password cracking
|
||||
attempts using the reset token, provided the secret isn't compromised.
|
||||
"""
|
||||
# Truncate microseconds so that tokens are consistent even if the
|
||||
# database doesn't support microseconds.
|
||||
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
|
||||
email_field_name = user.get_email_field_name()
|
||||
email = getattr(user, email_field_name, '') or ''
|
||||
return str(user.pk) + user.password + str(login_timestamp) + email + str(timestamp)
|
||||
```
|
||||
|
||||
### File: `tests/auth_tests/test_tokens.py`
|
||||
|
||||
**Test Added:** `TokenGeneratorTest.test_token_invalidated_after_email_change()`
|
||||
|
||||
```python
|
||||
def test_token_invalidated_after_email_change(self):
|
||||
"""
|
||||
The token is invalidated after the user changes their email address.
|
||||
"""
|
||||
user = User.objects.create_user('testuser', 'test@example.com', 'testpw')
|
||||
p0 = PasswordResetTokenGenerator()
|
||||
token = p0.make_token(user)
|
||||
# Token should be valid
|
||||
self.assertIs(p0.check_token(user, token), True)
|
||||
# Change the user's email address
|
||||
user.email = 'newemail@example.com'
|
||||
user.save()
|
||||
# Token should now be invalid
|
||||
self.assertIs(p0.check_token(user, token), False)
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Using `get_email_field_name()`
|
||||
- Introduced in Django 3.1
|
||||
- Supports custom user models that override the email field
|
||||
- Returns the correct field name (default: 'email', but can be customized)
|
||||
|
||||
### 2. Safe Email Retrieval
|
||||
```python
|
||||
email = getattr(user, email_field_name, '') or ''
|
||||
```
|
||||
- Handles users without an email field (falls back to empty string)
|
||||
- Works with any custom user model
|
||||
- Prevents AttributeError exceptions
|
||||
|
||||
### 3. Email Position in Hash
|
||||
```python
|
||||
str(user.pk) + user.password + str(login_timestamp) + email + str(timestamp)
|
||||
```
|
||||
- Email is added before the timestamp
|
||||
- Order is important for hash calculation
|
||||
- Maintains consistency across database implementations
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Results
|
||||
```
|
||||
Testing against Django installed in '/tmp/django-fix/django' with up to 48 processes
|
||||
Creating test database for alias 'default'...
|
||||
System check identified no issues (0 silenced).
|
||||
........
|
||||
----------------------------------------------------------------------
|
||||
Ran 8 tests in 0.005s
|
||||
|
||||
OK
|
||||
Destroying test database for alias 'default'...
|
||||
```
|
||||
|
||||
### All Tests Passing
|
||||
1. ✓ `test_make_token` - Basic token generation and validation
|
||||
2. ✓ `test_10265` - Token consistency for users created in same request
|
||||
3. ✓ `test_timeout` - Token expiration based on PASSWORD_RESET_TIMEOUT
|
||||
4. ✓ `test_check_token_with_nonexistent_token_and_user` - Validation with None inputs
|
||||
5. ✓ `test_token_with_different_secret` - Secret validation
|
||||
6. ✓ `test_token_default_hashing_algorithm` - Hash algorithm selection
|
||||
7. ✓ `test_legacy_token_validation` - Backward compatibility with old SHA1 tokens
|
||||
8. ✓ `test_token_invalidated_after_email_change` - **NEW** Email change invalidates token
|
||||
|
||||
## Security Analysis
|
||||
|
||||
### Vulnerabilities Mitigated
|
||||
1. **Password Reset Token Reuse After Email Change**
|
||||
- Before: User could use old token even after email change
|
||||
- After: Token becomes invalid when email changes
|
||||
|
||||
2. **Potential Account Takeover Vector**
|
||||
- Before: Attacker with old token could reset password if victim changed email
|
||||
- After: Old token no longer valid, preventing this attack
|
||||
|
||||
### Remaining Considerations
|
||||
1. **Email Spoofing**: Not addressed by this fix
|
||||
- Assumption: Email delivery system is trusted
|
||||
- Attackers cannot compromise email delivery without other means
|
||||
|
||||
2. **Race Conditions**: Minimal risk
|
||||
- Change happens instantly in database
|
||||
- Check happens at same time as change
|
||||
- Window for exploitation is negligible
|
||||
|
||||
3. **Backward Compatibility**: Breaking change (acceptable)
|
||||
- Existing tokens will become invalid
|
||||
- PASSWORD_RESET_TIMEOUT already limits token lifetime
|
||||
- Users will need to request new tokens after deployment
|
||||
- This is acceptable given security benefit
|
||||
|
||||
## Files Delivered
|
||||
|
||||
1. **IMPLEMENTATION_PLAN.md** - Detailed implementation plan and explanation
|
||||
2. **DJANGO_FIX_SUMMARY.md** - Technical summary of the fix
|
||||
3. **django_password_reset_token_fix.patch** - Unified diff patch file
|
||||
4. **test_password_reset_fix.py** - Demonstration script showing vulnerability and fix
|
||||
5. **SOLUTION_SUMMARY.md** - This file
|
||||
|
||||
## How to Apply the Fix
|
||||
|
||||
### Option 1: Apply the Patch
|
||||
```bash
|
||||
cd /path/to/django
|
||||
patch -p1 < django_password_reset_token_fix.patch
|
||||
```
|
||||
|
||||
### Option 2: Manual Application
|
||||
1. Edit `django/contrib/auth/tokens.py`
|
||||
2. Modify the `_make_hash_value()` method as shown above
|
||||
3. Edit `tests/auth_tests/test_tokens.py`
|
||||
4. Add the new test method as shown above
|
||||
|
||||
### Option 3: Copy Fixed Files
|
||||
Copy the implementation from `/tmp/django-fix/`:
|
||||
- `django/contrib/auth/tokens.py`
|
||||
- `tests/auth_tests/test_tokens.py`
|
||||
|
||||
## Verification
|
||||
|
||||
After applying the fix:
|
||||
|
||||
```bash
|
||||
# Run the specific test
|
||||
python tests/runtests.py auth_tests.test_tokens.TokenGeneratorTest.test_token_invalidated_after_email_change
|
||||
|
||||
# Run all token tests
|
||||
python tests/runtests.py auth_tests.test_tokens
|
||||
|
||||
# Run all auth tests
|
||||
python tests/runtests.py auth_tests
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- **Django Commit**: 7f9e4524d6b23424cf44fbe1bf1f4e70f6bb066e
|
||||
- **Issue**: Changing user's email could invalidate password reset tokens
|
||||
- **File Modified**: `django/contrib/auth/tokens.py`
|
||||
- **Test Added**: `tests/auth_tests/test_tokens.py`
|
||||
|
||||
## Conclusion
|
||||
|
||||
This fix addresses a security vulnerability in Django's password reset mechanism by including the user's email address in the token generation hash. The implementation:
|
||||
- ✓ Prevents token reuse after email change
|
||||
- ✓ Supports custom user models
|
||||
- ✓ Handles edge cases gracefully
|
||||
- ✓ Includes comprehensive test coverage
|
||||
- ✓ Maintains code quality and style
|
||||
- ✓ Is minimal and focused on the issue
|
||||
|
||||
The fix has been thoroughly tested and all tests pass successfully.
|
||||
Loading…
Add table
Reference in a new issue