説明
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オブジェクトを使ってください。1引数の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やパラメーター化APIを使えない場合に限り、保守されている文脈別のエンコーダーを使ってください。
- フィルターのアサーション値:Springの
LdapEncoder.filterEncode、ESAPIのencodeForLDAP(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