{"title":"LDAP Injection via Unsanitized Distinguished Name Construction","language":"Python","severity":"Critical","cwe":"CWE-90","source_lines":[5,6],"flow_lines":[5,6,8],"sink_lines":[8],"vulnerable_code":"import ldap\ndef fetch_device_config(device_id, org_unit):\n    conn = ldap.initialize('ldap://iot-directory.internal:389')\n    conn.simple_bind_s('cn=svcacct,dc=corp,dc=io', 'S3rvice!Pass')\n    dn_base = f'ou={org_unit},dc=corp,dc=io'\n    search_filter = f'(deviceSerial={device_id})'\n    attrs = ['configBlob', 'firmwareVer', 'accessToken']\n    res = conn.search_s(dn_base, ldap.SCOPE_SUBTREE, search_filter, attrs)\n    conn.unbind_s()\n    return res","explanation":"Both `org_unit` and `device_id` are directly interpolated into the LDAP Distinguished Name base (`dn_base`) and search filter (`search_filter`) without any sanitization or escaping. An attacker controlling either parameter can inject LDAP metacharacters (e.g., `*`, `)(`, `|`, `=`) to manipulate the DN structure or alter filter logic, bypassing intended access controls and accessing configurations/tokens across arbitrary OUs or devices.","remediation":"The fix addresses LDAP injection by applying three layers of defense: (1) input validation using a strict allowlist regex that rejects any characters outside alphanumerics, hyphens, and underscores; (2) proper escaping of `org_unit` via `ldap.dn.escape_dn_chars()` for DN construction and `device_id` via `ldap.filter.escape_filter_chars()` for search filter construction, neutralizing any LDAP metacharacters; (3) removal of hardcoded credentials in favor of environment variables. A `try/finally` block also ensures the LDAP connection is properly unbound even if an error occurs.","secure_code":"import ldap\nimport ldap.dn\nimport ldap.filter\nimport os\nimport re\n\n\ndef _validate_simple_name(value, field_name):\n    \"\"\"Validate that a value contains only safe alphanumeric characters, hyphens, and underscores.\"\"\"\n    if not value or not isinstance(value, str):\n        raise ValueError(f\"{field_name} must be a non-empty string\")\n    if not re.match(r'^[a-zA-Z0-9_-]+$', value):\n        raise ValueError(f\"{field_name} contains invalid characters\")\n    return value\n\n\ndef fetch_device_config(device_id, org_unit):\n    # Validate inputs against an allowlist of safe characters\n    org_unit = _validate_simple_name(org_unit, \"org_unit\")\n    device_id = _validate_simple_name(device_id, \"device_id\")\n\n    # Read credentials from environment variables instead of hardcoding\n    bind_dn = os.environ.get('LDAP_BIND_DN', 'cn=svcacct,dc=corp,dc=io')\n    bind_password = os.environ.get('LDAP_BIND_PASSWORD')\n    if not bind_password:\n        raise RuntimeError(\"LDAP_BIND_PASSWORD environment variable is not set\")\n\n    conn = ldap.initialize('ldap://iot-directory.internal:389')\n    try:\n        conn.simple_bind_s(bind_dn, bind_password)\n\n        # Escape the org_unit for safe use in the DN\n        escaped_org_unit = ldap.dn.escape_dn_chars(org_unit)\n        dn_base = f'ou={escaped_org_unit},dc=corp,dc=io'\n\n        # Escape the device_id for safe use in the LDAP search filter\n        escaped_device_id = ldap.filter.escape_filter_chars(device_id)\n        search_filter = f'(deviceSerial={escaped_device_id})'\n\n        attrs = ['configBlob', 'firmwareVer', 'accessToken']\n        res = conn.search_s(dn_base, ldap.SCOPE_SUBTREE, search_filter, attrs)\n    finally:\n        conn.unbind_s()\n\n    return res"}