LDAP 인젝션

C# LDAP 검색 필터 인젝션

설명

LDAP 검색 필터는 단순한 데이터 문자열이 아니라 &, |, !, *, 괄호 등으로 조건을 표현하는 문법입니다. 요청에서 받은 값을 System.DirectoryServices.DirectorySearcher.Filter, DirectorySearcher 생성자의 필터 인수 또는 System.DirectoryServices.Protocols.SearchRequest의 필터에 그대로 결합하면 공격자가 원래 조건 밖의 연산자나 와일드카드를 삽입할 수 있습니다.

예를 들어 애플리케이션이 (cn= + 사용자명 + )을 만들 때 사용자명이 *이면 결과 (cn=*)는 cn 속성이 존재하는 항목을 선택하는 조건이 됩니다. 더 복잡한 입력은 애플리케이션이 추가한 사용자·그룹·테넌트 조건의 의미를 바꿀 수 있습니다.

LDAP 검색 필터 값과 고유 이름(DN)은 서로 다른 문맥입니다. 필터의 어설션 값은 RFC 4515에 따라 이스케이프하고, DN의 속성 값은 RFC 4514에 따라 이스케이프해야 합니다. 한 문맥의 인코더를 다른 문맥에 대신 사용하면 안전하지 않습니다.

잠재적 영향

  • 사용자 또는 그룹 조회 조건을 넓혀 인증·권한 검사를 우회할 수 있습니다.
  • 디렉터리의 계정, 그룹, 전자우편 주소 등 허가되지 않은 정보를 조회할 수 있습니다.
  • 공격자가 비용이 큰 필터를 선택해 LDAP 서버와 애플리케이션 자원을 소모시킬 수 있습니다.
  • 쓰기 또는 관리 기능이 같은 동적 필터를 신뢰하면 의도하지 않은 디렉터리 개체가 수정될 수 있습니다.

해결 방법

  1. 클라이언트가 완성된 LDAP 필터, 속성명 또는 연산자를 보내도록 허용하지 않습니다. 검색 종류가 선택 가능해야 한다면 짧은 키를 서버가 소유한 고정 필터에 매핑합니다.
  2. 외부 값이 필터의 어설션 값에 반드시 들어가야 한다면 유지보수되는 LDAP 필터 빌더 또는 RFC 4515 인코더를 사용합니다. *, (, ), \, NUL을 올바른 \hh 옥텟 형식으로 이스케이프해야 합니다.
  3. .NET 10의 System.DirectoryServices 및 System.DirectoryServices.Protocols에는 공개된 기본 필터 이스케이퍼가 없습니다. 적합한 유지보수 라이브러리를 사용할 수 없다면 조직에서 승인한 하나의 RFC 4515 헬퍼로 구현을 중앙화하고, ASP.NET Core 10의 내부 구현과 RFC 예제를 기준으로 단위 테스트합니다.
  4. RFC 4514 DN 인코더, HTML·URL 인코더, 부분적인 Replace, 메서드 이름이 Sanitize 또는 Escape인 임의 헬퍼를 필터 인코딩의 증거로 간주하지 않습니다.
  5. LDAP 바인드 계정에 최소 권한을 부여하고, 검색 범위·반환 개수·페이지 크기·시간 제한을 적용합니다. 이러한 제한은 인코딩을 대체하지 않지만 공격 성공 시 영향을 줄입니다.

OWASP의 현재 LDAP 치트시트는 문맥에 맞는 인코더 사용을 권고하지만 C# 예시에서 2014년에 마지막으로 출시된 AntiXSS 4.3.0을 언급합니다. 기존 .NET Framework 코드의 Microsoft.Security.Application.Encoder.LdapFilterEncode는 필터 값용 레거시 API로 검토할 수 있지만, 새 .NET 10 애플리케이션에 이 오래된 패키지를 기본 해결책으로 추가하지 마세요.

예시

변경 전

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();
    }
}

변경 후

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();
    }
}

설명:

  • 변경 전: 요청의 username이 필터 문법 안에 그대로 들어가므로 공격자가 와일드카드나 추가 조건을 삽입할 수 있습니다.
  • 변경 후: 외부 입력은 서버가 작성한 두 개의 고정 필터 중 하나를 선택하는 키일 뿐이며 필터 문법이나 값으로 들어가지 않습니다. 결과 수와 서버 처리 시간도 제한합니다.

예시의 accountStatus는 실제 디렉터리 스키마에 맞게 조정하고, 각 조회에 필요한 권한도 별도로 확인하세요.

자유 형식의 값을 지원해야 하고 승인된 라이브러리가 없다면 다음과 같이 RFC 4515 필터 값 전용 헬퍼를 중앙에서 관리할 수 있습니다. 이 코드는 ASP.NET Core 10의 LDAP 클레임 조회 내부 구현과 같은 필수 문자를 이스케이프합니다.

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();
    }
}

이 헬퍼는 완성된 필터, 속성명, 연산자 또는 DN을 안전하게 만들지 않습니다. 필터 구조는 고정한 채 어설션 값 하나에만 적용하고, *, 괄호, 역슬래시, NUL 및 유니코드 입력을 단위 테스트해야 합니다.

참조