# PYTHON-DJANGO-SEC-081: Django Default Empty Password Value via flows()

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

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

## Description

This rule detects a specific Django password vulnerability pattern where
request.POST.get('password', '') -- using an empty string as the default value --
flows into set_password() without an intervening check for empty/None values.

This pattern is subtly dangerous because it appears to handle the case where the
password field is missing from the request. However, instead of rejecting the
missing password as an error, it silently defaults to an empty string and creates
an account accessible without any password.

The vulnerability manifests when a user omits the password field from a registration
form, or when an API client sends a request without the password parameter. Rather
than receiving an error, the user's account is created with an empty password hash,
effectively bypassing authentication.

This is distinct from SEC-080 (which detects literal empty strings) by specifically
tracking the data flow from the GET/POST default parameter through to the set_password
sink.


## 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
    password = request.POST.get('password')
    user = User.objects.get(id=1)
    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 register_user(request):
    username = request.POST.get('username', '')
    # SECURE: Use None default (no default) so missing password is explicit
    password = request.POST.get('password')
    if not username:
        return JsonResponse({'error': 'Username is required'}, status=400)
    # SECURE: Explicitly reject missing, empty, or whitespace-only passwords
    if not password or not password.strip():
        return JsonResponse({'error': 'Password is required'}, status=400)
    # SECURE: Validate password strength
    try:
        validate_password(password)
    except ValidationError as e:
        return JsonResponse({'errors': list(e.messages)}, status=400)
    user = User.objects.create_user(username=username, password=password)
    return JsonResponse({'user_id': user.id, 'status': 'created'})

def api_create_account(request):
    import json
    try:
        data = json.loads(request.body)
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)
    # SECURE: Require password in request body -- no default fallback
    password = data.get('password')
    if password is None:
        return JsonResponse({'error': 'Password field is required'}, status=400)
    if len(password) < 8:
        return JsonResponse({'error': 'Password must be at least 8 characters'}, status=400)
    user = User(username=data.get('username', ''))
    user.set_password(password)
    user.save()
    return JsonResponse({'user_id': user.id})

```

## 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-081",
    name="Django Default Empty Password Value",
    severity="HIGH",
    category="django",
    cwe="CWE-521",
    tags="python,django,password,default,OWASP-A07,CWE-521",
    message="Password default value may be empty string. Use None as default.",
    owasp="A07:2021",
)
def detect_django_default_empty_password():
    """Audit: detects request.POST.get with potential empty password default flowing to set_password."""
    return flows(
        from_sources=[
            calls("request.POST.get"),
            calls("*.POST.get"),
        ],
        to_sinks=[
            calls("*.set_password"),
        ],
        sanitized_by=[],
        propagates_through=PropagationPresets.standard(),
        scope="local",
    )
```

## How to Fix

- Use request.POST.get('password') without a default value, so missing fields return None rather than an empty string.
- After retrieving the password value, check if it is None, empty, or whitespace-only and return a 400 error immediately.
- Apply Django's validate_password() to enforce minimum strength requirements before calling set_password().
- Never use '' as the default value for password fields in request.POST.get() or request.GET.get() calls.
- For optional password fields in update forms, distinguish between "user wants to keep current password" (field absent) and "user wants to set a new password" (field present) using None/empty checks rather than defaulting to empty string.

## Security Implications

- **Silent Authentication Bypass on Missing Form Field:** When a registration form is submitted without the password field (intentionally
or due to a client-side bug), request.POST.get('password', '') returns '' and
set_password('') creates a valid empty-password hash. The user can log in
without providing any password, bypassing authentication silently with no
error logged.

- **API Client Misuse Creating Unprotected Accounts:** API clients that programmatically create user accounts by POSTing JSON may omit
the password field if their integration code has a bug. The server creates the
account with an empty password rather than rejecting the request, creating
unprotected accounts that an attacker can discover and use.

- **Batch User Provisioning Vulnerabilities:** Bulk user import scripts that read from CSV files or external APIs may produce
empty strings for missing password fields. If these empty strings flow to
set_password(), the batch creates unprotected accounts for the affected users.

- **Race Condition in Multi-Step Registration:** Multi-step registration flows that store partial state between steps may
temporarily create accounts with empty passwords if the password step is
bypassed or skipped due to a flow logic error. Empty string defaults in
set_password() calls make this transient state permanent.


## FAQ

**Q: How is this different from PYTHON-DJANGO-SEC-080?**

SEC-080 detects literal empty string '' passed directly to set_password() as a
constant value in the code. SEC-081 specifically tracks the data flow from
request.POST.get('password', '') where the empty string is the default for a
missing field, which then flows into set_password() without validation. SEC-081
catches the runtime vulnerability pattern that only manifests when the password
field is absent from the request.


**Q: Why is using None as the default in request.POST.get() safer?**

When request.POST.get('password') returns None (field not in POST data), a
subsequent check like if not password raises the expected error condition
before set_password() is called. When request.POST.get('password', '') returns ''
(field missing), if not password still evaluates to True ('' is falsy), so
the same check works -- but the dangerous case is when developers forget the
check and call set_password(password) directly.


**Q: Does Django's form validation prevent this vulnerability?**

Django's Form and ModelForm classes with required=True on the password field
will raise a ValidationError when the field is missing, preventing the empty
value from reaching set_password(). This vulnerability pattern occurs in views
that bypass the Form system and access request.POST directly, or in API views
that process JSON payloads without form validation.


**Q: How do I handle password update forms where the password field is optional?**

For "change password" forms where users can optionally update their password:
retrieve the value with request.POST.get('new_password') (no default), and if
the result is None, skip the set_password() call entirely (preserving the
current password). If the result is an empty string or whitespace, return an
error -- submitting an empty string for a required field is different from
omitting the field.


**Q: Can automated security testing catch this vulnerability?**

Yes. Send a registration request without the password field and verify the
response is an error (400 status) rather than a success. Then attempt to log
in with the created username and an empty password -- if login succeeds, the
vulnerability is confirmed. Both Code Pathfinder static analysis and this
integration test approach are needed for comprehensive coverage.


**Q: What Django forms-based alternative avoids this pattern entirely?**

Use Django's UserCreationForm or a custom ModelForm with a PasswordField for
registration. The form handles validation (including required field enforcement),
password confirmation matching, and strength validation via AUTH_PASSWORD_VALIDATORS.
The form's save() method calls set_password() with the validated value. This
approach eliminates the need to manually handle missing/empty passwords.


## References

- [CWE-521: Weak Password Requirements](https://cwe.mitre.org/data/definitions/521.html)
- [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.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 QueryDict.get() documentation](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.QueryDict.get)

---

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