설명
LDAP 삽입은 신뢰할 수 없는 데이터가 LDAP 검색 필터나 식별 이름(Distinguished Name, DN)의 값이 아니라 구문으로 해석될 때 발생합니다. 두 컨텍스트는 서로 다른 문법을 사용합니다.
- 검색 필터는 RFC 4515 문법을 사용합니다.
*,(,),\, NUL 같은 문자가 조건식을 바꾸며, 공격자가 속성 이름이나 매칭 규칙을 선택해도 필터 의미가 달라질 수 있습니다. - DN과 검색 베이스는 RFC 4514 문법을 사용합니다.
,,+,=,\같은 구분자와 메타문자가 대상 엔트리 또는 검색 하위 트리를 바꿀 수 있습니다.
따라서 필터용 인코딩을 DN에 재사용하거나, 완전한 공격자 제공 필터/DN 문자열을 파싱하는 것만으로는 안전해지지 않습니다.
잠재적 영향
디렉터리 권한과 검색 결과의 사용 방식에 따라 다음 문제가 발생할 수 있습니다.
- 인증 또는 인가 조건 우회
- 의도하지 않은 디렉터리 엔트리 조회 또는 변경
- 검색 범위 확대에 따른 정보 노출이나 자원 소모
해결 방법
1. LDAP 문법을 고정하고 값만 전달하기
JNDI에서는 개발자가 작성한 고정 필터 템플릿과 filterArgs 오버로드를 사용합니다. 문자열 인자는 필터 메타문자가 이스케이프되어 대입됩니다. JNDI 자리표시자는 값뿐 아니라 속성이나 매칭 규칙 위치에도 올 수 있으므로, 자리표시자를 어설션 값 위치에만 두고 속성, 매칭 규칙, 연산자 및 자리표시자 위치를 고정해야 합니다.
Spring LDAP 4.1.1에서는 LdapQueryBuilder.where(...).is(value), filter(fixedTemplate, values...) 또는 구조화된 Filter 객체를 사용합니다. 단일 인자 filter(String)은 입력을 검증하거나 이스케이프하지 않으므로 신뢰할 수 없는 문자열을 전달하면 안 됩니다. LdapQueryBuilder는 Spring Security가 아니라 Spring LDAP API입니다.
UnboundID LDAP SDK에서는 고정 속성과 값으로 Filter.createEqualityFilter("uid", value) 같은 Filter 객체를 만든 뒤 객체 오버로드에 전달합니다. Apache Directory에서는 FilterBuilder 같은 구조화 API를 사용합니다.
2. DN을 구성 요소로 만들기
DN과 검색 베이스는 고정된 속성 유형과 값으로 구성합니다. JNDI의 Rdn/LdapName, Spring LDAP의 LdapNameBuilder, 또는 UnboundID의 RDN/DN 구성 요소를 사용할 수 있습니다. new LdapName(untrustedCompleteDn)처럼 완전한 공격자 제공 DN을 파싱하는 것은 구문을 보존하므로 무력화가 아닙니다.
JNDI Context 메서드의 String 이름은 LDAP DN 문법뿐 아니라 JNDI 복합 이름(composite name) 문법으로도 해석됩니다. 이중 이스케이프 오류를 피하려면 LdapName을 문자열로 변환하지 말고 Name 오버로드에 직접 전달합니다. Spring LDAP의 LdapEncoder.nameEncode는 LDAP DN용 인코딩이며 JNDI 복합 이름을 위한 인코딩이 아니므로, JNDI의 String 이름에 단독 방어로 사용하면 안 됩니다.
3. 컨텍스트별 인코딩은 값에만 적용하기
구조화 또는 매개변수화 API를 사용할 수 없을 때만 유지보수되는 컨텍스트별 인코더를 사용합니다.
- 필터 어설션 값: Spring
LdapEncoder.filterEncode, ESAPIencodeForLDAP(value)또는encodeForLDAP(value, true)등 RFC 4515용 처리 - DN 값: RFC 4514용 DN 값 인코딩. JNDI 호출에는 가능하면 인코딩된 문자열 대신 구조화된
Name을 전달합니다.
ESAPI encodeForLDAP(value, false)는 와일드카드를 보존하므로 신뢰할 수 없는 정확 일치 값의 안전한 대체가 아닙니다. 와일드카드 검색이 제품 요구사항이라면 사용자가 필터 구문을 직접 제공하게 하지 말고, 서버가 정의한 명시적 검색 모드로 변환하며 결과 수와 시간을 제한합니다.
4. 문법 선택과 권한을 제한하기
사용자가 선택할 수 있는 속성, 매칭 규칙, 검색 범위 또는 기타 문법 요소는 유한한 서버 소유 허용 목록으로 매핑합니다. LDAP 연결은 최소 권한 계정으로 바인딩하고 클라이언트와 서버에 결과 크기 및 시간 제한을 설정합니다. 이러한 통제는 영향을 줄이는 심층 방어이며 LDAP 구문 자체를 무력화하지는 않습니다.
예시
JNDI 검색 필터
변경 전
import jakarta.servlet.http.HttpServletRequest;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
NamingEnumeration<SearchResult> searchUser(
DirContext context, HttpServletRequest request) throws NamingException {
String username = request.getParameter("username");
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String filter = "(&(uid=" + username + ")(objectClass=person))";
return context.search(
"ou=users,dc=example,dc=com", filter, controls);
}
username의 RFC 4515 메타문자가 필터 연산자나 추가 조건으로 해석될 수 있습니다.
변경 후
import jakarta.servlet.http.HttpServletRequest;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
NamingEnumeration<SearchResult> searchUser(
DirContext context, HttpServletRequest request) throws NamingException {
String username = request.getParameter("username");
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setCountLimit(100);
controls.setTimeLimit(2_000);
String fixedFilter = "(&(uid={0})(objectClass=person))";
return context.search(
"ou=users,dc=example,dc=com",
fixedFilter,
new Object[] {username},
controls);
}
필터 문법과 검색 베이스는 코드에 고정되어 있고 username은 어설션 값으로만 대입됩니다.
JNDI DN 구성
import jakarta.servlet.http.HttpServletRequest;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
Object lookupUser(DirContext context, HttpServletRequest request) throws NamingException {
String username = request.getParameter("username");
LdapName userDn = new LdapName("ou=users,dc=example,dc=com");
userDn.add(new Rdn("uid", username));
return context.lookup(userDn);
}
Rdn은 username을 DN 값으로 처리하고, lookup(Name)은 구조화된 이름을 JNDI 복합 이름 문자열로 다시 해석하지 않습니다.
Spring LDAP 쿼리
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.ldap.query.LdapQuery;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
LdapQuery userQuery(HttpServletRequest request) {
String username = request.getParameter("username");
return query()
.base("ou=users,dc=example,dc=com")
.countLimit(100)
.timeLimit(2_000)
.where("uid").is(username)
.and("objectClass").is("person");
}
속성 이름과 필터 구조는 서버가 정하고, username은 빌더가 값 컨텍스트에 맞게 처리합니다.
운영 설정 점검
애플리케이션 코드와 함께 디렉터리 ACL, 바인딩 계정의 권한, 서버 측 검색 제한을 확인하세요. 검색 결과를 인증이나 인가에 사용하는 경우 해당 판단도 별도로 검토하세요.
참조
- CWE-90: Improper Neutralization of Special Elements used in an LDAP Query
- OWASP LDAP Injection Prevention Cheat Sheet
- OWASP Top 10:2025 A05 - Injection
- OWASP ASVS 5.0 V1.2.6
- RFC 4515: LDAP Search Filters
- RFC 4514: LDAP Distinguished Names
- Java SE 25
DirContext - Java SE 25
Rdn - Oracle JNDI Tutorial: Handling Special Characters
- Spring LDAP 4.1.1: Advanced LDAP Queries
- Spring LDAP
LdapEncoder - UnboundID LDAP SDK for Java 7.0.5
Filter - Apache Directory
FilterBuilder