# PYTHON-DJANGO-SEC-080: Django Empty Password in set_password()

> **Severity:** HIGH | **CWE:** CWE-521, CWE-258 | **OWASP:** A07:2021

- **Language:** Python
- **Category:** Django
- **URL:** https://codepathfinder.dev/registry/python/django/PYTHON-DJANGO-SEC-080
- **Detection:** `pathfinder scan --ruleset python/PYTHON-DJANGO-SEC-080 --project .`

## Description

This rule detects cases in Django applications where set_password() is called with
an empty string literal ('') as the password argument, which creates user accounts
with no password protection.

Django's set_password() stores a hashed version of the provided password in the
database. When called with an empty string, it stores a valid hash of the empty string,
meaning the account is accessible without providing any password. This is distinctly
different from the correct way to create passwordless accounts: using None (which
Django stores as an unusable password hash "!") or calling set_unusable_password()
explicitly.

The rule uses both pattern matching (calls("*.set_password") audit) and taint analysis
to catch cases where the empty string default value flows from request.POST.get('password', '')
into set_password().


## Vulnerable Code

```python
from django.contrib.auth.models import User

# SEC-080: set_password with empty string
def reset_password_empty(user):
    user.set_password("")
    user.save()


# SEC-081: POST data flowing to set_password
    user.set_password(password)
    user.save()
```

## Secure Code

```python
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.http import JsonResponse

def create_user(request):
    username = request.POST.get('username', '')
    password = request.POST.get('password')  # No default -- require explicit value
    if not password:
        return JsonResponse({'error': 'Password is required'}, status=400)
    # SECURE: Validate password strength before setting
    try:
        validate_password(password)
    except ValidationError as e:
        return JsonResponse({'error': list(e.messages)}, status=400)
    user = User.objects.create_user(username=username, password=password)
    return JsonResponse({'user_id': user.id})

def create_service_account(request):
    username = request.POST.get('username', '')
    user = User.objects.create(username=username)
    # SECURE: Use set_unusable_password() to explicitly disable password login
    user.set_unusable_password()
    user.save()
    return JsonResponse({'user_id': user.id, 'password_login': False})

def reset_password(request):
    password = request.POST.get('new_password')
    if not password:
        return JsonResponse({'error': 'New password is required'}, status=400)
    user = request.user
    # SECURE: Set the actual provided password, validated for strength
    try:
        validate_password(password, user)
    except ValidationError as e:
        return JsonResponse({'error': list(e.messages)}, status=400)
    user.set_password(password)
    user.save()
    return JsonResponse({'status': 'Password updated'})

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets


@python_rule(
    id="PYTHON-DJANGO-SEC-080",
    name="Django Empty Password in set_password()",
    severity="HIGH",
    category="django",
    cwe="CWE-521",
    tags="python,django,password,empty,OWASP-A07,CWE-521",
    message="Empty password set via set_password(). Use None instead of empty string.",
    owasp="A07:2021",
)
def detect_django_empty_password():
    """Audit: detects set_password() calls that may use empty strings."""
    return calls("*.set_password")
```

## How to Fix

- Never call set_password('') with an empty string; use set_unusable_password() when password-based login should be disabled.
- Use None as the password value when creating accounts that should not be accessible via password, as Django stores this as the unusable password marker.
- Always validate password input with validate_password() before calling set_password() to enforce minimum strength requirements.
- Use request.POST.get('password') without a default value (or default to None) so that missing passwords are caught as an error rather than silently set to empty.
- Add Django's built-in password validators in AUTH_PASSWORD_VALIDATORS settings to enforce complexity requirements application-wide.

## Security Implications

- **Authentication Bypass via Empty Password:** An account created with set_password('') allows anyone to log in by submitting
an empty password field. Depending on the application's authentication flow,
this may allow unauthorized access to admin accounts, service accounts, or
user accounts created through automated workflows.

- **Privilege Escalation via Service Account Takeover:** Service accounts or integration accounts sometimes have passwords set to empty
strings during development or provisioning. If these accounts have elevated
privileges (staff=True, is_superuser=True), an empty password creates a trivial
privilege escalation vector for any authenticated or unauthenticated user.

- **Password Reset Flow Bypass:** If a password reset workflow calls set_password('') as an intermediate step
(e.g., to "clear" the password before sending a reset link), the account is
unprotected during the time between clearing and the user setting a new password.

- **Automated Account Provisioning Vulnerability:** Automated user provisioning scripts that create accounts with empty passwords
as placeholders create a window of vulnerability. If a provisioning job runs
but the user setup is never completed, the empty-password account remains
accessible indefinitely.


## FAQ

**Q: What is the actual security difference between set_password('') and set_unusable_password()?**

set_password('') hashes the empty string and stores it as a valid password hash.
Any authentication attempt with an empty password field will succeed against this
hash. set_unusable_password() stores '!' as the password hash, which never matches
any real password string. Django's authentication backend explicitly checks for
unusable passwords and returns False for any authenticate() call, even with an
empty password argument.


**Q: Can this vulnerability be triggered unintentionally through form processing?**

Yes. The common pattern request.POST.get('password', '') passes an empty string
default. If this value is used directly in set_password() without checking
whether the field was actually submitted, and if the form is submitted without
a password value, the account receives an empty password. Always use None as
the default and check for None before calling set_password().


**Q: Does Django's password validation prevent setting an empty password?**

Only if validate_password() is called explicitly before set_password(). Django's
MinimumLengthValidator (minimum 8 characters) raises a ValidationError for empty
strings. However, validate_password() is not called automatically by set_password()
-- it must be called explicitly in the view. This is why the bug can occur even
in applications that have password validators configured in AUTH_PASSWORD_VALIDATORS.


**Q: Are there legitimate use cases for set_password('') in tests?**

No. In tests, use User.objects.create_user(username='test', password='testpass123')
which creates an account with a known password for testing. For testing scenarios
where you need an unusable password, use user.set_unusable_password(). Never use
set_password('') even in tests, as it can propagate to production if test helpers
are reused in deployment scripts.


**Q: How does Django detect that a password is unusable?**

Django's AbstractBaseUser.has_usable_password() returns False if the password
field starts with '!'. set_unusable_password() stores UNUSABLE_PASSWORD_PREFIX + 
UNUSABLE_PASSWORD_SUFFIX as the hash. Django's check_password() and authenticate()
use has_usable_password() to short-circuit before checking the hash, ensuring
accounts with unusable passwords can never be authenticated via password.


**Q: What should we do about existing accounts that have empty passwords?**

Run a database query to find all accounts where the password hash matches the hash
of an empty string (Python: check(make_password('')) against stored hashes). Force
password resets for all such accounts immediately, or call set_unusable_password()
on them and require them to go through a password creation flow on next login.


## References

- [CWE-521: Weak Password Requirements](https://cwe.mitre.org/data/definitions/521.html)
- [CWE-258: Empty Password in Configuration File](https://cwe.mitre.org/data/definitions/258.html)
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
- [Django Password Management](https://docs.djangoproject.com/en/stable/topics/auth/passwords/)
- [Django Password Validators](https://docs.djangoproject.com/en/stable/ref/settings/#auth-password-validators)
- [Django set_unusable_password()](https://docs.djangoproject.com/en/stable/ref/contrib/auth/#django.contrib.auth.models.User.set_unusable_password)

---

Source: https://codepathfinder.dev/registry/python/django/PYTHON-DJANGO-SEC-080
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
