Ldap Injection in Django with Basic Auth
Ldap Injection in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
LDAP Injection occurs when an attacker can manipulate LDAP query construction by injecting malicious input. In Django, this risk can surface in custom authentication or group-mapping logic even when you use HTTP Basic Authentication. Basic Auth supplies a username and password via the Authorization header; Django typically validates credentials and then may call into an LDAP backend or a custom directory service to authorize users. If the code builds LDAP filter strings by concatenating user-controlled values without proper escaping, the injected filter can change query semantics, bypass restrictions, or extract sensitive directory data.
Consider a scenario where Django’s authentication pipeline uses a custom LDAP backend after Basic Auth credentials are parsed. A developer might construct an LDAP filter like this:
(&(objectClass=person)(uid={username})(userPassword={password}))
If username comes directly from the Basic Auth credential and is not escaped, an attacker can supply a username such as admin)(uid=*), producing the filter:
(&(objectClass=person)(uid=admin)(uid=*)(userPassword=...))
This injected wildcard can return multiple entries, potentially allowing the attacker to authenticate as another user or enumerate directory contents. Even when Django’s default authentication is not used, custom decorators or permission checks that query LDAP based on Basic Auth identities remain vulnerable if they embed input directly into filters. Common LDAP injection techniques include filter assertion manipulation, distinguished name (DN) injection to alter scope, and leveraging special characters to break filter structure. Because Basic Auth transmits credentials in base64 (not encryption), transport security is essential, but injection is a server-side construction issue. The attack surface is present when LDAP queries embed user-supplied input in filters, search bases, or attribute values without strict validation or escaping.
Another realistic pattern is using the username to dynamically select a base DN for search:
search_base = f"ou=people,dc=example,dc=com,uid={username}"
An attacker can inject additional segments or close the DN prematurely, leading to unauthorized directory access or information disclosure. Because LDAP supports complex escaping rules for special characters like (, ), *, and \, ad-hoc string concatenation is unsafe. Always prefer parameterized APIs or well-tested escaping routines. The combination of Django’s Basic Auth flow and LDAP integration requires rigorous input sanitization to prevent injection, privilege escalation, or data exposure.
Basic Auth-Specific Remediation in Django — concrete code fixes
Secure LDAP usage with Basic Auth in Django starts with avoiding string concatenation for LDAP filters. Use library-provided escaping and parameterized search APIs. Below are concrete, working examples that demonstrate safe patterns.
Safe LDAP filter construction with python-ldap
Use ldap.filter.filter_format to safely embed values into filters:
import ldap
from django.conf import settings
def safe_ldap_auth(username, password):
# username and password come from Basic Auth credentials
ldap_uri = settings.LDAP_URI
conn = ldap.initialize(ldap_uri)
# Use filter_format to escape special characters in username
user_filter = ldap.filter.filter_format(
'(&(objectClass=person)(uid=%s)(userPassword=%s))',
[username, password]
)
try:
conn.simple_bind_s(username, password)
# Perform a safe search using parameterized filter
result = conn.search_s(
settings.LDAP_BASE_DN,
ldap.SCOPE_SUBTREE,
filterstr=user_filter
)
return bool(result)
except ldap.INVALID_CREDENTIALS:
return False
finally:
conn.unbind_s()
Safe dynamic base DN with proper escaping
If you must derive parts of the search base from user input, validate and encode each component:
import re
def safe_search_base(username):
# Allow only alphanumeric and a few safe characters; reject path separators
if not re.match(r'^[a-zA-Z0-9._-]+$', username):
raise ValueError('Invalid username for directory lookup')
# Use a fixed structure and append the validated component as a single RDN
return f"uid={username},ou=people,dc=example,dc=com"
# Usage inside a view or permission check
def my_protected_view(request):
auth = request.META.get('HTTP_AUTHORIZATION')
if auth and auth.startswith('Basic '):
import base64
decoded = base64.b64decode(auth.split(' ')[1]).decode('utf-8')
username, password = decoded.split(':', 1)
base = safe_search_base(username)
# Proceed with ldap.initialize and conn.search_s using base
# ... rest of view
Django settings and custom authentication backend
Define a custom backend that uses the safe LDAP helper and integrate it into Django’s authentication flow:
# settings.py
AUTHENTICATION_BACKENDS = [
'myapp.auth.LdapBackend',
'django.contrib.auth.backends.ModelBackend',
]
# myapp/auth.py
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model
class LdapBackend(BaseBackend):
def authenticate(self, request, username=None, password=None):
if safe_ldap_auth(username, password):
User = get_user_model()
user, _ = User.objects.get_or_create(username=username)
return user
return None
def get_user(self, user_id):
User = get_user_model()
return User.objects.filter(pk=user_id).first()
Additional hardening
- Always use TLS (ldaps) to protect credentials in transit, since Basic Auth encodes but does not encrypt.
- Validate and sanitize any input used in search bases, even when it originates from trusted claims.
- Apply principle of least privilege for the LDAP bind account used by Django.
These patterns keep user-controlled data out of raw LDAP filter strings and ensure directory queries remain well-formed, reducing the risk of LDAP injection while working with Basic Auth in Django.