# LDAP Injection via Unescaped Search Filter Construction

Language: Python
Severity: Critical
CWE: CWE-90

## Source
3

## Flow
3-6-7

## Sink
7

## Vulnerable Code
```python
import ldap

def retrieve_iot_device_credentials(device_mac, tenant_id):
    ldap_conn = ldap.initialize('ldap://iot-directory.internal:389')
    ldap_conn.simple_bind_s('cn=iot-reader,dc=company,dc=com', 'P@ssw0rd')
    search_filter = f"(&(objectClass=iotDevice)(macAddress={device_mac})(tenantID={tenant_id}))"
    results = ldap_conn.search_s('ou=devices,dc=company,dc=com', ldap.SCOPE_SUBTREE, search_filter, ['deviceKey', 'apiToken'])
    if results:
        device_dn, device_attrs = results[0]
        return {'key': device_attrs['deviceKey'][0].decode(), 'token': device_attrs['apiToken'][0].decode()}
    return None
```

## Explanation

The function accepts user-controlled parameters (device_mac and tenant_id) and directly interpolates them into an LDAP search filter using f-strings without any sanitization or escaping. An attacker can inject malicious LDAP filter syntax to bypass authentication checks, access unauthorized devices across tenants, or extract sensitive credential data.

## Remediation

The fix applies defense-in-depth by first validating that the MAC address and tenant ID conform to expected formats using regex patterns, then escaping any remaining LDAP special characters using ldap.filter.escape_filter_chars() before interpolating them into the search filter. This prevents attackers from injecting malicious LDAP filter syntax such as wildcards or parentheses that could bypass tenant isolation or extract unauthorized credentials.

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

def validate_mac_address(mac):
    """Validate MAC address format before using in LDAP filter."""
    mac_pattern = re.compile(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$')
    if not mac_pattern.match(mac):
        raise ValueError(f"Invalid MAC address format: {mac}")
    return mac

def validate_tenant_id(tenant_id):
    """Validate tenant ID format (alphanumeric and hyphens only)."""
    tenant_pattern = re.compile(r'^[a-zA-Z0-9\-]+$')
    if not tenant_pattern.match(tenant_id):
        raise ValueError(f"Invalid tenant ID format: {tenant_id}")
    return tenant_id

def retrieve_iot_device_credentials(device_mac, tenant_id):
    # Validate input formats
    validate_mac_address(device_mac)
    validate_tenant_id(tenant_id)
    
    # Escape special LDAP filter characters to prevent injection
    safe_mac = escape_filter_chars(device_mac)
    safe_tenant_id = escape_filter_chars(tenant_id)
    
    ldap_conn = ldap.initialize('ldap://iot-directory.internal:389')
    ldap_conn.simple_bind_s('cn=iot-reader,dc=company,dc=com', 'P@ssw0rd')
    search_filter = '(&(objectClass=iotDevice)(macAddress={})(tenantID={}))'.format(safe_mac, safe_tenant_id)
    results = ldap_conn.search_s('ou=devices,dc=company,dc=com', ldap.SCOPE_SUBTREE, search_filter, ['deviceKey', 'apiToken'])
    if results:
        device_dn, device_attrs = results[0]
        return {'key': device_attrs['deviceKey'][0].decode(), 'token': device_attrs['apiToken'][0].decode()}
    return None
```
