1.3 디렉토리 서비스 조회 및 결과 검증

개요

디렉토리 서비스(예: LDAP - Lightweight Directory Access Protocol)는 사용자 정보, 시스템 설정 등 다양한 데이터를 계층적으로 저장하고 조회하는 데 사용됩니다. 애플리케이션이 디렉토리 서비스에 질의할 때 외부 입력값을 필터나 검색 조건으로 사용하는 경우, 입력값을 적절히 처리하지 않으면 LDAP 삽입(LDAP Injection) 공격이 발생할 수 있습니다.

LDAP 삽입은 SQL 삽입과 유사하게, 공격자가 필터 구문에 악의적인 메타 문자를 삽입하여 질의문의 논리를 변경하는 공격입니다. 이를 통해 인증을 우회하거나, 비인가된 정보를 조회/수정/삭제하는 것이 가능해집니다.

또한, 디렉토리 서비스 조회 결과를 사용할 때 그 유효성을 검증하지 않으면, 잘못된 데이터 처리로 인해 애플리케이션 오류나 정보 노출이 발생할 수 있습니다. 이 기준은 디렉토리 서비스 조회 시 입력값 처리와 결과 검증에 대한 보안 요구사항을 정의합니다.

보안 대책

  • - 입력값 검증 및 이스케이프: LDAP 필터 구문에 사용되는 외부 입력값에서 LDAP 메타 문자(예: *, (, ), \, NUL)를 필터링하거나, 각 언어/라이브러리가 제공하는 이스케이프 함수를 사용하여 안전하게 처리합니다.
  • - 프레임워크/라이브러리 활용: 안전한 LDAP 질의 생성을 지원하는 프레임워크나 라이브러리(예: OWASP ESAPI)를 활용합니다.
  • - 최소 권한 원칙: 디렉토리 서비스 접근 계정에 필요한 최소한의 권한(조회, 수정 등)만 부여합니다.
  • - 결과 검증: 조회 결과가 예상한 개수, 형식, 내용과 일치하는지 확인합니다. 특히 인증 목적으로 사용할 경우, 단 하나의 유효한 결과만 반환되었는지 엄격하게 검증해야 합니다.

코드 예시 (Java - JNDI 사용)

취약한 코드:

import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.NamingEnumeration;

// 사용자 입력(username)을 직접 필터 문자열에 결합
String filter = "(&(uid=" + username + ")(objectClass=user))";
SearchControls sc = new SearchControls();
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);

// LDAP 삽입 공격에 취약 (예: username = "*)(uid=*))(|(uid=*)")
NamingEnumeration results = ctx.search("ou=users,dc=example,dc=com", filter, sc); 
                        
안전한 코드 (이스케이프 처리):

import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.NamingEnumeration;

// LDAP 필터 메타 문자를 이스케이프하는 유틸리티 함수 (예시)
public static String escapeLDAPFilter(String filter) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < filter.length(); i++) {
        char curChar = filter.charAt(i);
        switch (curChar) {
            case '\\': sb.append("\\5c"); break;
            case '*':  sb.append("\\2a"); break;
            case '(':  sb.append("\\28"); break;
            case ')':  sb.append("\\29"); break;
            case '\u0000': sb.append("\\00"); break; // NUL character
            default: sb.append(curChar);
        }
    }
    return sb.toString();
}

// 이스케이프된 사용자 입력을 사용하여 필터 생성
String safeUsername = escapeLDAPFilter(username);
String filter = "(&(uid=" + safeUsername + ")(objectClass=user))"; 
SearchControls sc = new SearchControls();
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);

// LDAP 삽입 방어됨
NamingEnumeration results = ctx.search("ou=users,dc=example,dc=com", filter, sc); 

// 결과 검증 (예: 인증 시 단일 결과만 유효)
if (results.hasMoreElements()) {
    SearchResult sr = results.next();
    if (results.hasMoreElements()) {
        // 결과가 두 개 이상이면 인증 실패 처리
        throw new SecurityException("Multiple users found for the given username.");
    }
    // 단일 결과 처리 ...
} else {
    // 결과 없음 처리 ...
}
                        

Python(python-ldap, ldap3), C#(System.DirectoryServices), PHP 등 다른 언어에서도 LDAP 필터 구성 시 입력값을 안전하게 이스케이프하는 것이 중요합니다. 각 라이브러리가 제공하는 이스케이프 함수나 방식을 확인하여 적용해야 합니다.