Ldap Injection in Fastapi with Cockroachdb
Ldap Injection in Fastapi with Cockroachdb — how this specific combination creates or exposes the vulnerability
LDAP Injection occurs when an attacker can manipulate LDAP query construction, typically through unsanitized user input. In a Fastapi application that uses an LDAP server for authentication or group membership checks, if input is concatenated directly into LDAP filter strings, attackers can bypass authentication or extract sensitive data. When this Fastapi service stores or references account data in Cockroachdb, the LDAP interaction often maps user identities to relational records (e.g., mapping authenticated LDAP DN to tenant or user rows in Cockroachdb). This introduces a multi-layer risk: an LDAP injection can lead to unauthorized directory access and can be chained to manipulate or infer Cockroachdb data through application logic that trusts LDAP-derived identifiers.
Consider a Fastapi endpoint that takes a username and builds an LDAP filter without escaping:
from fastapi import Fastapi, Depends, HTTPException
import ldap
app = Fastapi()
def get_ldap_conn():
conn = ldap.initialize("ldap://ldap.example.com")
conn.protocol_version = ldap.VERSION3
return conn
@app.post("/login")
async def login(username: str, password: str):
conn = get_ldap_conn()
# Unsafe: direct string interpolation into LDAP filter
filt = f"(uid={username})"
try:
conn.simple_bind_s(filt, password)
except ldap.INVALID_CREDENTIALS:
raise HTTPException(status_code=400, detail="Invalid credentials")
# If bind succeeds, map to Cockroachdb identity
cur = conn.cursor() # placeholder for application DB session
# Example: query Cockroachdb for user metadata using LDAP-supplied identity
cur.execute("SELECT id, role FROM users WHERE ldap_uid = $1", (username,))
row = cur.fetchone()
return {"ok": True, "role": row[1] if row else None}
The (uid={username}) filter is vulnerable to LDAP Injection. An attacker can supply uid*)(uid=admin)) or other control characters to change the filter semantics, potentially authenticating as another user or enumerating directory entries. Because the application later uses the same username to query Cockroachdb, the LDAP injection can influence which Cockroachdb rows are accessed, leading to horizontal privilege escalation (BOLA/IDOR) or unauthorized data exposure. The combination is particularly risky if Cockroachdb holds sensitive PII or if LDAP group membership is used to derive row-level permissions in the database.
Moreover, Fastapi’s dependency injection and async patterns can inadvertently propagate the tainted input. For example, passing the username from authentication to multiple service functions increases the attack surface across components that interact with Cockroachdb. Without input validation and strict separation between identity store queries and database queries, the LDAP channel becomes a pivot point for attacks against the relational store.
Cockroachdb-Specific Remediation in Fastapi — concrete code fixes
Remediation centers on preventing LDAP injection by treating LDAP filter construction as a parameterized, escaped operation, and ensuring Cockroachdb queries remain independent and parameterized. Do not concatenate user input into LDAP filters. Instead, use an LDAP escape routine and bind with a constructed distinguished name (DN) or use parameterized filters where the LDAP library supports it. For Cockroachdb, always use parameterized SQL; never interpolate values into queries, even when values originate from LDAP.
Below is a secure Fastapi pattern that separates concerns, escapes LDAP input, and uses strongly typed parameters for Cockroachdb:
from fastapi import Fastapi, Depends, HTTPException
import ldap
import psycopg2
from psycopg2 import sql
app = Fastapi()
def get_ldap_conn():
conn = ldap.initialize("ldap://ldap.example.com")
conn.protocol_version = ldap.VERSION3
return conn
def escape_ldap_value(value: str) -> str:
"""Escape RFC4515 special characters for safe LDAP filter use."""
# From RFC4515: * -> \2a, ( -> \28, ) -> \29, \\ -> \5c, null -> \00
replacements = {('*', r'\2a'), ('(', r'\28'), (')', r'\29'), ('\\', r'\5c'), ('\x00', r'\00')}
result = value
for char, esc in replacements:
result = result.replace(char, esc)
return result
@app.post("/login")
async def login(username: str, password: str):
# Validate and normalize input before using in LDAP
safe_username = escape_ldap_value(username.strip())
# Use a parameterized approach by constructing a full DN when possible
# Example: avoid filter injection by binding directly with a known DN pattern
user_dn = f"uid={safe_username},ou=people,dc=example,dc=com"
conn = get_ldap_conn()
try:
conn.simple_bind_s(user_dn, password)
except ldap.INVALID_CREDENTIALS:
raise HTTPException(status_code=400, detail="Invalid credentials")
# Now independently query Cockroachdb using parameterized SQL only
conn_pg = psycopg2.connect(
host="cockroachdb.example.com",
port26257,
user="app_user",
password="secure_password",
database="app_db"
)
try:
with conn_pg.cursor() as cur:
# Safe: parameterized query, no string interpolation
cur.execute(
sql.SQL("SELECT id, role FROM users WHERE ldap_uid = %s"),
(safe_username,)
)
row = cur.fetchone()
return {"ok": True, "role": row[1] if row else None}
finally:
conn_pg.close()
Key points specific to Cockroachdb:
- Always use
cursor.execute(sql, params)with parameter placeholders; Cockroachdb’s PostgreSQL wire protocol supports this viapsycopg2or compatible drivers. - Do not construct SQL with string concatenation or interpolation, even if the input was escaped for LDAP, because threat contexts differ.
- Validate and sanitize input once at the edge (e.g., allow only alphanumeric and limited special characters for usernames) to reduce complexity downstream.
- If using an ORM or higher-level DB access layer, ensure it uses parameterized statements under the hood; avoid raw string queries.
Additionally, apply defense-in-depth by implementing schema-level constraints in Cockroachdb (e.g., NOT NULL, CHECK constraints) and using least-privilege database roles for the application user. This ensures that even if a higher-layer bug introduces tainted input, the database restricts what can be done with it.