LDAP injection

LDAP search filter injection in C#

Description

LDAP search filters use syntax such as &, |, !, * and parentheses to express conditions. Concatenating request values into System.DirectoryServices.DirectorySearcher.Filter, a DirectorySearcher constructor filter or a System.DirectoryServices.Protocols.SearchRequest filter can let an attacker insert operators or wildcards outside the intended condition.

For example, when the application constructs (cn= + username + ), a username of * produces (cn=*), a presence filter that selects entries with a cn attribute. More complex input can change user, group or tenant conditions added by the application.

LDAP filter values and distinguished names (DNs) are different contexts. Escape filter assertion values according to RFC 4515 and DN attribute values according to RFC 4514. An encoder for one context is not interchangeable with the other.

Potential impact

  • Broader user or group searches can bypass authentication or authorization checks.
  • Unauthorized directory information, including accounts, groups and email addresses, may be disclosed.
  • Expensive filters can consume LDAP server and application resources.
  • Write or administrative operations that trust the same dynamic filter may modify unintended directory objects.

Remediation

  1. Do not let clients supply complete filters, attribute names or operators. Map short search identifiers to server-owned fixed filters.
  2. If external data must appear in an assertion value, use a maintained filter builder or RFC 4515 encoder. Escape *, (, ), \ and NUL using the correct \hh octet representation.
  3. .NET 10's System.DirectoryServices and System.DirectoryServices.Protocols have no public built-in filter escaper. If a suitable maintained library is unavailable, centralize an organization-approved RFC 4515 helper and test it against the RFC examples and ASP.NET Core 10's internal implementation.
  4. Do not substitute RFC 4514 DN encoding, HTML/URL encoding, partial Replace calls or arbitrary helpers named Sanitize or Escape for filter encoding.
  5. Use a least-privileged LDAP bind account and limit search scope, result count, page size and time. These limits reduce impact but do not replace encoding.

OWASP's LDAP cheat sheet recommends context-specific encoding, but its C# example refers to AntiXSS 4.3.0, last released in 2014. Microsoft.Security.Application.Encoder.LdapFilterEncode can be reviewed as a legacy filter-value API in existing .NET Framework applications; do not add this old package as the default solution for new .NET 10 applications.

Examples

Before

csharp
using Microsoft.AspNetCore.Mvc;
using System.DirectoryServices;

public sealed class DirectoryController
{
    public SearchResult? Find(
        [FromQuery] string username,
        DirectorySearcher searcher)
    {
        searcher.Filter =
            "(&(objectClass=user)(cn=" + username + "))";

        return searcher.FindOne();
    }
}

After

csharp
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.DirectoryServices;

public sealed class DirectoryController
{
    private static readonly IReadOnlyDictionary<string, string> Filters =
        new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["active-users"] = "(&(objectClass=user)(accountStatus=active))",
            ["disabled-users"] = "(&(objectClass=user)(accountStatus=disabled))"
        };

    public SearchResult? Find(
        [FromQuery] string filterKey,
        DirectorySearcher searcher)
    {
        if (!Filters.TryGetValue(filterKey, out var filter))
        {
            throw new ArgumentException("Unsupported filter.", nameof(filterKey));
        }

        searcher.Filter = filter;
        searcher.SizeLimit = 100;
        searcher.ServerTimeLimit = TimeSpan.FromSeconds(2);
        return searcher.FindOne();
    }
}

The first example inserts username directly into filter syntax. The second uses input only to select one of two server-authored filters and limits results and server processing time. Adapt accountStatus to the actual directory schema and check permission for each search separately.

If free-form values are needed and no approved library is available, centralize a helper dedicated to RFC 4515 filter values. This example escapes the same required characters as ASP.NET Core 10's internal LDAP claims lookup.

csharp
using System;
using System.Text;

internal static class LdapFilterValue
{
    internal static string Encode(string value)
    {
        ArgumentNullException.ThrowIfNull(value);

        var escaped = new StringBuilder(value.Length);
        foreach (var character in value)
        {
            escaped.Append(character switch
            {
                '\\' => @"\5c",
                '*' => @"\2a",
                '(' => @"\28",
                ')' => @"\29",
                '\0' => @"\00",
                _ => character.ToString()
            });
        }

        return escaped.ToString();
    }
}

This helper does not make complete filters, attribute names, operators or DNs safe. Apply it only to one assertion value within a fixed filter, and test *, parentheses, backslashes, NUL and Unicode input.

References