# LDAP Injection

Language: Python
Severity: Critical
CWE: CWE-90

## Source
5

## Flow
5-6-8

## Sink
8

## Vulnerable Code
```python
import ldap
def fetch_device_config(device_id, org_unit):
    conn = ldap.initialize('ldap://iot-directory.internal:389')
    conn.simple_bind_s('cn=svc_iot,dc=corp,dc=local', 'S3rv!ceP@ss')
    base_dn = f'ou={org_unit},dc=corp,dc=local'
    search_filter = f'(deviceId={device_id})'
    attrs = ['deviceConfig', 'firmwareVersion', 'accessToken']
    result = conn.search_s(base_dn, ldap.SCOPE_SUBTREE, search_filter, attrs)
    conn.unbind_s()
    return result
```

## Explanation

Both `device_id` and `org_unit` parameters are function arguments accepted from external callers (untrusted input). They are interpolated directly into the LDAP base DN (line 5) and search filter (line 6) using f-strings without any sanitization or escaping, and then consumed by `conn.search_s()` on line 8. An attacker can inject LDAP metacharacters into either parameter to manipulate the directory search scope or filter logic.

## Remediation

The fix applies three layers of defense: (1) strict input validation via regex allowlists that reject any characters outside alphanumerics, hyphens, and underscores for both `device_id` and `org_unit`; (2) LDAP DN escaping via `ldap.dn.escape_dn_chars()` for `org_unit` before constructing the base DN; and (3) LDAP filter escaping via `ldap.filter.escape_filter_chars()` for `device_id` before constructing the search filter. This prevents both DN traversal and filter injection attacks while also adding proper resource cleanup with a try/finally block.

## Secure Code
```python
import ldap
from ldap.filter import escape_filter_chars
from ldap.dn import escape_dn_chars
import re

def fetch_device_config(device_id, org_unit):
    # Validate input format: only allow alphanumeric, hyphens, and underscores
    if not re.match(r'^[a-zA-Z0-9_-]+$', device_id):
        raise ValueError("Invalid device_id: contains disallowed characters")
    if not re.match(r'^[a-zA-Z0-9_-]+$', org_unit):
        raise ValueError("Invalid org_unit: contains disallowed characters")

    conn = ldap.initialize('ldap://iot-directory.internal:389')
    conn.simple_bind_s('cn=svc_iot,dc=corp,dc=local', 'S3rv!ceP@ss')

    # Escape DN special characters in org_unit to prevent DN injection
    safe_org_unit = escape_dn_chars(org_unit)
    base_dn = f'ou={safe_org_unit},dc=corp,dc=local'

    # Escape LDAP filter special characters in device_id to prevent filter injection
    safe_device_id = escape_filter_chars(device_id)
    search_filter = f'(deviceId={safe_device_id})'

    attrs = ['deviceConfig', 'firmwareVersion', 'accessToken']
    try:
        result = conn.search_s(base_dn, ldap.SCOPE_SUBTREE, search_filter, attrs)
    finally:
        conn.unbind_s()
    return result
```
