{"title":"LDAP Injection via Unsanitized Filter String Construction","language":"Python","severity":"Critical","cwe":"CWE-90","source_lines":[2],"flow_lines":[2,6,7],"sink_lines":[7],"vulnerable_code":"from ldap3 import Server, Connection, ALL\ndef fetch_iot_device_owner(device_serial, base_dn='ou=devices,dc=iotcloud,dc=local'):\n    srv = Server('ldap://iot-directory.internal', get_info=ALL)\n    conn = Connection(srv, user='cn=svcReader,dc=iotcloud,dc=local', password='R3ad0nly!')\n    conn.bind()\n    ldap_qry = f'(&(objectClass=iotDevice)(serialNumber={device_serial}))'\n    conn.search(base_dn, ldap_qry, attributes=['ownerUid', 'deviceRole', 'accessToken'])\n    if conn.entries:\n        rec = conn.entries[0]\n        return {'owner': str(rec['ownerUid']), 'role': str(rec['deviceRole']), 'token': str(rec['accessToken'])}\n    return None","explanation":"The `device_serial` parameter is accepted directly from the caller (untrusted external input) and interpolated without any escaping or sanitization into the LDAP search filter string via an f-string on line 6. This tainted filter is then passed directly to `conn.search()` on line 7, allowing an attacker to inject LDAP metacharacters that alter the filter logic and potentially dump all records including sensitive `accessToken` fields.","remediation":"The fix applies two layers of defense against LDAP injection. First, the input is validated against a strict allowlist regex that only permits alphanumeric characters, hyphens, and underscores—rejecting any input containing LDAP metacharacters like `*`, `(`, `)`, `|`, or `\\`. Second, `ldap3.utils.conv.escape_filter_chars()` is used to escape any remaining LDAP-special characters in the device serial before it is interpolated into the filter string, ensuring that even if validation were bypassed, the input would be treated as a literal value rather than filter syntax.","secure_code":"from ldap3 import Server, Connection, ALL\nfrom ldap3.utils.conv import escape_filter_chars\nimport re\n\ndef fetch_iot_device_owner(device_serial, base_dn='ou=devices,dc=iotcloud,dc=local'):\n    # Validate device_serial format (alphanumeric, hyphens, and underscores only)\n    if not device_serial or not re.match(r'^[a-zA-Z0-9_\\-]+$', device_serial):\n        raise ValueError(\"Invalid device serial number format\")\n\n    srv = Server('ldap://iot-directory.internal', get_info=ALL)\n    conn = Connection(srv, user='cn=svcReader,dc=iotcloud,dc=local', password='R3ad0nly!')\n    conn.bind()\n\n    # Escape LDAP special characters to prevent injection\n    safe_serial = escape_filter_chars(device_serial)\n    ldap_qry = '(&(objectClass=iotDevice)(serialNumber={}))'.format(safe_serial)\n\n    conn.search(base_dn, ldap_qry, attributes=['ownerUid', 'deviceRole', 'accessToken'])\n    if conn.entries:\n        rec = conn.entries[0]\n        return {'owner': str(rec['ownerUid']), 'role': str(rec['deviceRole']), 'token': str(rec['accessToken'])}\n    return None"}