説明
LDAP検索フィルターは、&、|、!、*、括弧などを使って条件を表す構文です。リクエストの値をSystem.DirectoryServices.DirectorySearcher.Filter、DirectorySearcherコンストラクターのフィルター引数、System.DirectoryServices.Protocols.SearchRequestのフィルターにそのまま連結すると、攻撃者が意図した条件の外に演算子やワイルドカードを挿入できる可能性があります。
例えば、(cn= + ユーザー名 + )を組み立てる場合、ユーザー名が*なら結果は(cn=*)となり、cn属性が存在する項目を選択します。より複雑な入力で、アプリケーションが付け加えたユーザー、グループ、テナントの条件を変えられる可能性もあります。
LDAPフィルターの値と識別名(DN)は異なる文脈です。フィルターのアサーション値はRFC 4515、DNの属性値はRFC 4514に従ってエスケープしてください。一方のエンコーダーを他方に流用できません。
想定される影響
- ユーザーやグループの検索範囲を広げ、認証や認可の確認を回避される可能性があります。
- アカウント、グループ、メールアドレスなど、権限のないディレクトリ情報を取得される可能性があります。
- 負荷の高いフィルターでLDAPサーバーやアプリケーションの資源を消費される可能性があります。
- 書き込みや管理処理が同じ動的フィルターを信頼すると、意図しないオブジェクトを変更される可能性があります。
対処方法
- 完成したフィルター、属性名、演算子をクライアントに指定させないでください。検索を選ぶ短い識別子を、サーバーが管理する固定のフィルターに対応付けます。
- 外部の値をアサーション値に使う場合は、保守されているフィルタービルダーかRFC 4515エンコーダーを使ってください。
*、(、)、\、NULを正しい\hhのオクテット表現に変換します。 - .NET 10の
System.DirectoryServicesとSystem.DirectoryServices.Protocolsには公開された標準のフィルターエスケーパーがありません。適切な保守済みライブラリーがなければ、組織で承認したRFC 4515ヘルパーに集約し、RFCの例やASP.NET Core 10の内部実装を基にテストしてください。 - RFC 4514のDNエンコーダー、HTML・URLエンコーダー、部分的な
Replace、名前がSanitizeやEscapeというだけのヘルパーで代用しないでください。 - LDAPのバインドアカウントを最小権限にし、検索範囲、返却数、ページサイズ、時間を制限してください。影響を減らす対策であり、エンコーディングの代わりにはなりません。
OWASPのLDAPチートシートは文脈に適したエンコードを勧めていますが、C#の例では2014年に最後のリリースがあったAntiXSS 4.3.0を参照しています。既存の.NET FrameworkではMicrosoft.Security.Application.Encoder.LdapFilterEncodeを従来のフィルター値用APIとして検討できます。ただし、新しい.NET 10アプリケーションの標準的な対策として、この古いパッケージを追加しないでください。
例
変更前
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();
}
}
変更後
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をフィルターの構文に直接挿入します。変更後は、サーバーが作成した2種類のフィルターを選ぶキーとしてだけ使い、結果数とサーバーの処理時間も制限します。accountStatusは実際のディレクトリスキーマに合わせ、検索ごとの権限も別途確認してください。
自由形式の値が必要で、承認済みライブラリーがない場合は、RFC 4515のフィルター値専用ヘルパーに処理を集約できます。次の例は、ASP.NET Core 10のLDAPクレーム検索の内部実装と同じ必須文字をエスケープします。
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を安全にするものではありません。構造を固定し、1つのアサーション値だけに適用してください。*、括弧、バックスラッシュ、NUL、Unicodeの入力をテストしてください。
参考資料
- Microsoft CA3005: LDAPインジェクションの確認
- DirectorySearcher.Filter (.NET 10)
- System.DirectoryServices 10.0.0
- System.DirectoryServices.Protocols 10.0.0
- .NET LDAPフィルターのエスケープAPI提案
- ASP.NET Core 10のRFC 4515フィルター値エスケープ実装
- OWASP LDAPインジェクション防止チートシート
- OWASP インジェクション防止チートシート
- OWASP ASVS 5.0.0 V1.2.6
- RFC 4515: LDAP検索フィルターの文字列表現
- RFC 4514: LDAP識別名の文字列表現
- CWE-90: LDAPインジェクション
- OWASP Top 10:2025 A05 Injection
- AntiXSS 4.3.0のパッケージ履歴
- KISA ソフトウェアセキュリティ弱点診断ガイド2021