Skip to content

Commit c0166d9

Browse files
Prevent small risk of Token overwrite (#9754)
* Fix #9250: Prevent token overwrite and improve security - Fix key collision issue that could overwrite existing tokens - Use force_insert=True only for new token instances - Replace os.urandom with secrets.token_hex for better security - Add comprehensive test suite to verify fix and backward compatibility - Ensure existing tokens can still be updated without breaking changes * Fix code style: remove trailing whitespace and unused imports * Fix #9250: Prevent token overwrite with minimal changes - Add force_insert=True to Token.save() for new objects to prevent overwriting existing tokens - Revert generate_key method to original implementation (os.urandom + binascii) - Update tests to work with original setUp() approach - Remove verbose comments and unrelated changes per reviewer feedback * Fix flake8 violations: remove extra blank lines and trailing whitespace * Update tests/test_authtoken.py Co-authored-by: Bruno Alla <browniebroke@users.noreply.github.com> * Update tests/test_authtoken.py Co-authored-by: Bruno Alla <browniebroke@users.noreply.github.com> * Update tests/test_authtoken.py Co-authored-by: Bruno Alla <browniebroke@users.noreply.github.com> * Fix token key regeneration behavior and add test * Update tests/test_authtoken.py Co-authored-by: Bruno Alla <browniebroke@users.noreply.github.com> --------- Co-authored-by: Bruno Alla <browniebroke@users.noreply.github.com>
1 parent 92a2c4d commit c0166d9

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

rest_framework/authtoken/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,17 @@ class Meta:
2727
verbose_name_plural = _("Tokens")
2828

2929
def save(self, *args, **kwargs):
30+
"""
31+
Save the token instance.
32+
33+
If no key is provided, generates a cryptographically secure key.
34+
For new tokens, ensures they are inserted as new (not updated).
35+
"""
3036
if not self.key:
3137
self.key = self.generate_key()
38+
# For new objects, force INSERT to prevent overwriting existing tokens
39+
if self._state.adding:
40+
kwargs['force_insert'] = True
3241
return super().save(*args, **kwargs)
3342

3443
@classmethod

tests/test_authtoken.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.contrib.admin import site
66
from django.contrib.auth.models import User
77
from django.core.management import CommandError, call_command
8+
from django.db import IntegrityError
89
from django.test import TestCase, modify_settings
910

1011
from rest_framework.authtoken.admin import TokenAdmin
@@ -48,6 +49,45 @@ def test_whitespace_in_password(self):
4849
self.user.save()
4950
assert AuthTokenSerializer(data=data).is_valid()
5051

52+
def test_token_creation_collision_raises_integrity_error(self):
53+
user2 = User.objects.create_user('user2', 'user2@example.com', 'p')
54+
existing_token = Token.objects.create(user=user2)
55+
56+
# Try to create another token with the same key
57+
with self.assertRaises(IntegrityError):
58+
Token.objects.create(key=existing_token.key, user=self.user)
59+
60+
def test_key_generated_on_save_when_cleared(self):
61+
# Create a new user for this test to avoid conflicts with setUp token
62+
user2 = User.objects.create_user('test_user2', 'test2@example.com', 'password')
63+
64+
# Create a token without a key - it should generate one automatically
65+
token = Token(user=user2)
66+
token.key = "" # Explicitly clear the key
67+
token.save()
68+
69+
# Verify the key was generated
70+
self.assertEqual(len(token.key), 40)
71+
self.assertEqual(token.user, user2)
72+
73+
def test_clearing_key_on_existing_token_raises_integrity_error(self):
74+
"""Test that clearing the key on an existing token raises IntegrityError."""
75+
user = User.objects.create_user('test_user3', 'test3@example.com', 'password')
76+
token = Token.objects.create(user=user)
77+
token.key = ""
78+
79+
# This should raise IntegrityError because:
80+
# 1. We're trying to update a record with an empty primary key
81+
# 2. The OneToOneField constraint would be violated
82+
with self.assertRaises(Exception): # Could be IntegrityError or DatabaseError
83+
token.save()
84+
85+
def test_saving_existing_token_without_changes_does_not_alter_key(self):
86+
original_key = self.token.key
87+
88+
self.token.save()
89+
self.assertEqual(self.token.key, original_key)
90+
5191

5292
class AuthTokenCommandTests(TestCase):
5393

0 commit comments

Comments
 (0)