LDAPインジェクション(CWE-90)の仕組みと対策。検索フィルタとDNでエスケープ規則が違う点まで整理する
対象の目安: 社内認証やディレクトリ連携を扱う開発者 / 実務

社内の利用者情報やグループ情報をディレクトリサーバーに置き、ログイン画面の認証や権限の照会をLDAPで済ませている業務システムは今も数多くあります。このとき、入力されたログインIDを検索フィルタの文字列にそのまま連結すると、フィルタの文法上の記号がデータではなく命令として読まれ、問い合わせの意味そのものが書き換わります。これがLDAPインジェクションです。MITREはこの欠陥をCWE-90として整理しています。
やっかいなのは、SQLインジェクション対策として普及したプレースホルダの発想が、LDAPではそのまま使えない点です。OWASPのLDAP Injection Prevention Cheat Sheetは、この欠陥が頻出する理由として、より安全なパラメータ化されたLDAPクエリのインターフェースが乏しいことと、LDAPが利用者認証に広く使われていることの2点を挙げています。この記事では、認証処理を題材にした発生機構と、実務で踏みやすい取り違えを開発者向けに整理します。
LDAPインジェクションが何を書き換えるのか
MITREのCWE-90は、正式名称を「Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')」といい、上流から受け取った外部影響下の入力を使ってLDAPクエリの全部または一部を組み立てる際に、下流へ送られる時点で意図したLDAPクエリを変えてしまう特殊要素を無害化しない、あるいは無害化が不十分である欠陥と定義されています。抽象度はBaseで、親はデータ問い合わせ論理の特殊要素に関するCWE-943です。同じ親の下にSQLインジェクション(CWE-89)が並んでいると捉えると位置づけがつかめます。
OWASP Top 10の2021年版でも、A03:2021 Injectionに対応づけられた33個のCWEにCWE-90が含まれています。A03が挙げる対策は、データを命令や問い合わせから分離すること、安全なAPIやパラメータ化されたインターフェースを使うこと、サーバー側で許可リスト方式の入力検証を行うこと、残った特殊文字をその処理系の規則でエスケープすることです。
検索フィルタの文法が意味を変える理由
LDAPの検索フィルタは、RFC 4515「Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters」(2006年6月)が定める前置記法の文字列です。(&(cn=A)(sn=B)) が論理積、(|(cn=A)(sn=B)) が論理和、(!(cn=A)) が否定を表し、(cn=*) のように値の位置へ書いたアスタリスクは、その属性が存在する全エントリに一致する記号として働きます。括弧とアンパサンド、縦棒、感嘆符、アスタリスクは、フィルタ文字列の中では構造と演算を担う記号だということです。
したがって (uid= と ) の間に利用者の入力を素のまま挟むと、入力に含まれた括弧やアスタリスクがそのまま構造の一部として読まれます。OWASPのWeb Security Testing Guideは、searchfilter="(cn="+user+")" という組み立てに * を1文字与えるだけでフィルタが (cn=*) になり、cn属性を持つ全オブジェクトに一致してしまう例を挙げています。
SQLインジェクションとの違い
SQLインジェクションは、WHERE name = '入力値' のような文字列にシングルクォートや論理演算子を混ぜて命令とデータの境界を壊す欠陥です。原理はLDAPインジェクションと同じで、命令を組み立てる文字列にデータを溶かし込んだ結果として起こります。違うのは対策の道具立てです。SQLには、命令の構造を先にデータベースへ渡して値を後から束縛するプリペアドステートメントという標準の仕組みがあります。LDAPのワイヤ形式では、検索フィルタはASN.1の構造体として送られます(RFC 4511第4.5.1.7節)。ところがアプリケーションが実際に触るのはRFC 4515の文字列表現であることが多く、その文字列を組み立てる標準のパラメータ化インターフェースは規定されていません。値の分離は各言語のライブラリやフレームワークの実装に委ねられています。
そのためLDAPでの防御は、フィルタの骨組みをコード側で固定し、可変部分を用途に応じたエスケープ関数へ通す形になります。JavaのJNDIのように、フィルタ文字列に {0} のような変数を書いて値を配列で渡すとエスケープしてくれるAPIもありますが、これは言語側が用意した支援であってプロトコル標準のパラメータ化ではありません。使えるかどうかは採用しているクライアントライブラリ次第です。
あわせて読みたい
SQLインジェクションとは何か。仕組み・攻撃手法・影響・対策を原理から徹底解説
認証画面で認証がすり抜ける機構
LDAP認証の実装でよく見かけるのが、利用者名とパスワードの両方をひとつの検索フィルタに埋め込み、一致するエントリが1件返ればログイン成功と判定する作りです。フィルタは (&(uid=入力されたID)(userPassword=入力されたパスワード)) のような論理積になります。
ここで入力されたIDに閉じ括弧やアスタリスク、論理演算子を含む文字列が渡ると、開発者が書いた論理積の中へ攻撃者の条件が割り込みます。OWASPのWeb Security Testing Guideは、この形の認証フィルタに構造を壊す入力を与えるとフィルタが常に真となる条件に変わり、パスワードが正しくなくても最初に見つかったLDAP利用者としてログインが通る例を示しています。開発者が書いた論理積が入力の途中で閉じられ、そこまでで常に真となるフィルタが完成します。パスワード側の条件は、その後ろに続く別の論理和のなかへ取り込まれて評価の対象から外れるため、判定に効かなくなるのが機構です。
同じ理屈は、認証を通った後の検索画面にも当てはまります。氏名で社員を検索するフィルタに * を渡せば全件が返り、絞り込みの条件を論理和で無効化すれば表示範囲外のエントリまで列挙できます。認証回避だけでなく、組織情報の広範な開示にもつながります。
注意
ここで扱う挙動の確認は、自分が管理するディレクトリサーバー、または書面で許可を得た検証環境に対してのみ行ってください。許可のないシステムへの試行は不正アクセス禁止法をはじめとする法令に抵触します。本記事の記述は、自組織の実装を点検する目的で読んでください。
フィルタとDNでエスケープ規則が違う
LDAPのエスケープには、検索フィルタの値に対するものと、DN(識別名)の属性値に対するものの2種類があります。OWASPのLDAP Injection Prevention Cheat Sheetはこの2つを別のものとして扱い、フィルタ用の入力を無害化しているのか、資格情報のようにDNとして使う値を無害化しているのかで正しいエスケープ関数を選び分けるよう求めています。
| 用途 | 準拠する規格 | エスケープが必要な文字 | 表記 |
|---|---|---|---|
| 検索フィルタの値 | RFC 4515 | * ( ) \ とNUL | バックスラッシュに続く2桁の16進数(\2a \28 \29 \5c \00) |
| DNの属性値 | RFC 4514 | " + , ; < > \ とNUL、先頭の空白と #、末尾の空白 | バックスラッシュに続く2桁の16進数(NULは \00 のみ)。空白と " # + , ; < = > \ はバックスラッシュ前置でも可 |
RFC 4515の第3節は、アスタリスク(0x2a)、左括弧(0x28)、右括弧(0x29)、バックスラッシュ(0x5c)、NUL(0x00)を表すオクテットを、バックスラッシュに続く2桁の16進数で表現すると定めています(あわせて、正しいUTF-8列を構成しない0x7F超のオクテットもエスケープするよう求めています)。これによりフィルタ解析の曖昧さが取り除かれます。
DN側はRFC 4514「Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names」(2006年6月、Standards Track)が定めており、この文書はRFC 2253を廃止しています。第2.4節がエスケープ対象とするのは、文字列の先頭に現れる空白(U+0020)と番号記号(U+0023)、末尾に現れる空白、" + , ; < > \ の各文字、そしてNUL文字です。オクテットごとにバックスラッシュと2桁の16進数へ置き換えるのが基本で、空白と " # + , ; < = > \ に限ってはバックスラッシュを前置する書き方も許されます。DNエスケープの根拠としてRFC 2253を挙げる古い資料が残っているため、ライブラリを選ぶときはどちらの規則で書かれているかを確認します。
そのうえで避けたいのが用途の取り違えです。フィルタ用のエスケープをDNへ適用しても、RDNを区切るカンマやプラス記号は対象外のまま通過するため、DN構造の書き換えは止められません。逆にDN用のエスケープをフィルタへ適用しても、アスタリスクと括弧はRFC 4514のエスケープ対象に含まれないため、フィルタ構造の書き換えを止められるとは限りません。
実装で組み立てる7つの対策
対策は、フィルタとDNの構造をコード側で固定し、値だけを正しい経路で通す方針に集約されます。次の順で組み立てます。
- 1
検索フィルタへ入力を文字列連結しない
フィルタの骨組みはコードに固定し、可変部分は専用のエスケープ関数を通した値だけにします。文字列連結でフィルタを作る実装が残っていないかを、まずコードベース全体で洗い出します。
- 2
フィルタ用とDN用のエスケープを使い分ける
RFC 4515の規則で処理すべき値と、RFC 4514の規則で処理すべき値をコード上で分けます。同じ関数を両方へ流用しないこと、検索の基点となるベースDNを利用者の入力から作らないことを設計の約束にします。
- 3
バインドは検索してから行う2段構えにする
ログインIDを
uid=入力値,ou=users,dc=example,dc=comのように連結してDNを作るのではなく、最小権限のアカウントで検索し、一意に定まったエントリのDNを取り出して、そのDNと入力されたパスワードでバインドします。DNの組み立てに利用者入力を関与させなければ、DN側の注入経路が構造的に消えます。 - 4
バインドアカウントの権限を最小化する
検索に使うバインドアカウントには、読み取り対象の枝と属性を絞った権限だけを与え、書き込みや管理の権限は付けません。OWASPのチートシートも、成立時の被害を抑える多層防御としてこれを挙げています。
- 5
許可リスト方式で入力を検証する
ログインIDのように書式が決まっている項目は、英数字と一部の記号だけを許す正規表現などでLDAPへ渡す前に検証します。OWASPのInput Validation Cheat Sheetは、検証の前に文字列を正規の符号化へそろえておくことも求めています。検証はエスケープの代わりにならないため、両方を行います。
- 6
エラーメッセージから情報を返さない
バインド失敗の理由やディレクトリの応答をそのまま画面に出すと、フィルタが壊れたか否かの差分が手がかりになります。利用者へ返すのは一律の失敗メッセージにとどめます。
- 7
LDAPSまたはStartTLSで通信を保護する
simple bindはパスワードを平文で送るため、経路の保護が前提になります。RFC 4513は、保護されていない資格情報を開放的なネットワークで使うことを強く非推奨とし、TLSなどで機密性と完全性が守られていない限りそうした認証方式を既定で有効にすべきではないと述べています。
言語ごとの安全な書き方
公式ドキュメントで実在を確認できた仕組みだけを挙げます。
JavaのJNDIには、フィルタ式に {i} 形式の変数を書いて値を配列で渡す search(String name, String filterExpr, Object[] filterArgs, SearchControls cons) があります(検索の起点を Name で受け取る同名のオーバーロードも同じ挙動です)。Java SE 8のAPIドキュメントは、文字列の引数が変数に代入されるとき、フィルタ内で特別な意味を持つ文字(アスタリスクなど)はRFC 2254の規則に従ってエスケープされたものとして解釈される、と説明しています。RFC 2254は現在のRFC 4515に置き換えられていますが、エスケープ対象の文字と表記は同じです。
String base = "ou=users,dc=example,dc=com";
// 脆弱: 入力をフィルタ文字列へ連結している
String filter = "(&(uid=" + userInput + ")(objectClass=person))";
NamingEnumeration<SearchResult> results = ctx.search(base, filter, controls);
// 安全: 骨組みを固定し、値はパラメータとして渡す
String safeFilter = "(&(uid={0})(objectClass=person))";
NamingEnumeration<SearchResult> safeResults =
ctx.search(base, safeFilter, new Object[]{ userInput }, controls);
Spring LDAPでは LdapQueryBuilder の filter(String filterFormat, String... params) を使います。リファレンスは、このメソッドが指定文字列を MessageFormat の入力として扱いパラメータを適切にエンコードして挿入すると説明し、引数を取らない filter(String) は利用者入力からフィルタを組み立てる用途に向かないと注意しています。低レベルの処理には org.springframework.ldap.support.LdapEncoder があり、filterEncode がフィルタ用、nameEncode がDN用です。
Pythonのldap3では、ldap3.utils.conv の escape_filter_chars() を通した値だけをフィルタへ入れます。公式ドキュメントは、search() へ渡す前に利用者入力に対してこの関数を呼ぶよう明記し、RFC 4515の第3節に沿ってアスタリスクを \2A、左括弧を \28、右括弧を \29、バックスラッシュを \5C、NULを \00 へ変換すると説明しています。利用者入力からDNを組み立てる場合は、ldap3.utils.dn の escape_rdn() を各RDNの属性値へ適用するよう別途案内されています。検索結果として受け取った完成済みのDNは、そのままバインドに使う値ですので、この関数へ通しません。
PHPには ldap_escape(string $value, string $ignore = "", int $flags = 0): string があり、PHP 5.6.0以降で利用できます。LDAP_ESCAPE_FILTER を指定すると ldap_search() に渡すフィルタ用、LDAP_ESCAPE_DN を指定するとDN用としてエスケープされます。用途でフラグを選ぶ設計のため、どちらを渡しているかをコードレビューで確認します。
.NETについては、OWASPのチートシートがクエリ構築時に自動でLDAPエンコードを行うライブラリとしてLINQ to LDAPを挙げています(対応は.NET Framework 4.5以下という注記付き)。エスケープ関数にはAntiXSSライブラリの Encoder.LdapFilterEncode(string) と Encoder.LdapDistinguishedNameEncode(string) があり、前者はRFC 4515、後者はRFC 2253に従うとMicrosoftのアーカイブ記事に記されています。いずれも公開が古いため、採用中のランタイムで維持されているかを確認してから採否を決めます。
エスケープと入力検証の役割分担については、次の記事で整理しています。
あわせて読みたい
入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする
匿名バインドと未認証バインドの落とし穴
OWASPのチートシートは、バインド認証を有効にすれば資格情報の検証と認可のチェックが働くとしたうえで、匿名接続や未認証バインドを悪用してこれを回避される余地が残ると注意しています。この2つの区別はRFC 4513「Lightweight Directory Access Protocol (LDAP): Authentication Methods and Security Mechanisms」(2006年6月)が定めています。匿名バインド(第5.1.1節)は名前もパスワードも長さゼロで送るBind要求、未認証バインド(第5.1.2節)は長さゼロでない識別名を送りながらパスワードだけ長さゼロで送る形です。後者の識別名はログなどの追跡用途のためだけに使われることが意図されており、認証にも認可にも使ってはならないとされています。
危険なのは、Bind操作の成功だけを見てログイン成功と判定する実装です。RFC 4513の第6.3.1節は、サーバーが未認証のBind要求に成功応答を返す実装があり得るため、クライアントが識別名の認証に成功したと誤認しながら実際には匿名の認可状態が確立されているだけ、という事態が起こると述べています。同節は、simple bindの結果を認可の判断に使うクライアントは、渡されたパスワードが空でないことを検証して未認証Bind要求を能動的に検出すべきだとしています。
あわせて読みたい
認証と認可の違い。Authentication と Authorization を基礎から整理する
まとめ
LDAPインジェクションは、外部から与えられた入力が検索フィルタやDNの特殊要素として解釈され、問い合わせの意図した論理が書き換わる欠陥です(CWE-90)。フィルタの括弧やアスタリスクが構造と演算を担う記号である以上、値の位置に素の入力を置いた時点で、認証の条件を常に真に変えたり本来見えないエントリを列挙したりする余地が生まれます。防御はフィルタ構造の固定と用途別のエスケープが軸です。検索フィルタはRFC 4515、DNはRFC 4514という規格の違いを取り違えないこと、認証では入力からDNを組み立てず検索してからバインドすること、バインドアカウントを最小権限に絞ることの3点を設計段階の約束事にしておくと実装が安定します。
LDAPインジェクション対策チェックリスト
- 検索フィルタを文字列連結で組み立てている箇所をコードベースから洗い出したか
- フィルタ用(RFC 4515)とDN用(RFC 4514)のエスケープを用途ごとに使い分けているか
- ログインIDからDNを連結せず、最小権限アカウントで検索してから得たDNでバインドしているか
- バインドアカウントの読み取り範囲と属性を絞り、書き込み権限を与えていないか
- 許可リスト方式の入力検証を、エスケープと併用してサーバー側で実施しているか
- 認証失敗の応答を一律にし、ディレクトリの詳細なエラーを画面へ返していないか
- パスワードが空のBind要求を検出して拒否し、LDAPSまたはStartTLSを必須にしているか
同じ根を持つ他のインジェクションと並べて読むと、命令とデータを分離するという原理の使い回し方が見えてきます。
あわせて読みたい
NoSQLインジェクションの仕組みと対策。クエリ演算子の注入で認証回避やデータ抽出に至る欠陥を塞ぐ
あわせて読みたい
OWASP Top 10とは。開発者が押さえるべきWebの代表的リスクと対策を一気に理解する
出典・参考
- CWE-90: Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection') (MITRE)
- OWASP LDAP Injection Prevention Cheat Sheet
- RFC 4515: LDAP: String Representation of Search Filters (IETF)
- RFC 4514: LDAP: String Representation of Distinguished Names (IETF)
- RFC 4513: LDAP: Authentication Methods and Security Mechanisms (IETF)
- OWASP Top 10 2021 A03:2021 Injection
- OWASP Input Validation Cheat Sheet
- RFC 4511: LDAP: The Protocol (IETF)
- Microsoft Archive: AntiXSS 4.0 Released (LdapFilterEncode / LdapDistinguishedNameEncode)
- OWASP Web Security Testing Guide: Testing for LDAP Injection
- Java SE 8 API: javax.naming.directory.DirContext
関連する記事
SQLインジェクションとは何か。仕組み・攻撃手法・影響・対策を原理から徹底解説
代表的なWeb脆弱性であるSQLインジェクションを、なぜ起きるのかという原理から、攻撃手法の分類、想定される影響、根本対策であるプレースホルダの使い方、多層防御、検出方法までを実務目線で網羅的に解説します。
NoSQLインジェクションの仕組みと対策。クエリ演算子の注入で認証回避やデータ抽出に至る欠陥を塞ぐ
NoSQLインジェクション(CWE-943相当)を発生原理から対策まで整理します。信頼できない入力がクエリのオブジェクト構造や演算子として解釈され、認証回避やデータ抽出に至る仕組みと、型検証や演算子の拒否、安全なクエリAPI、$whereの不使用、最小権限による根本対策を開発者向けに解説します。
入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする
セキュアコーディングの土台である入力バリデーションと出力エスケープを、それぞれの役割の違いから整理します。入口の検証は防御の一段目、出口の文脈別エスケープがXSSやインジェクションの根本対策である理由を、原理と実務の判断基準まで掘り下げます。


