NoSQL・LDAPインジェクション対策
検索式そのものを入力として受け取らず、アプリが定めた条件へ値だけを入れる方法を説明します。
説明
検索APIへJSONを渡す場合でも、値の中のオブジェクトが演算子として扱われることがあります。LDAPでは文字列の特殊文字が検索条件の構造を変えます。それぞれの構文に合わせた対処が必要です。
二人の固定レコードに対し、文書検索とLDAP検索を比較します。mongomockとldap3の固定ディレクトリを使い、型検証とフィルター値のエスケープによって返る対象がどう変わるかを調べます。
検索式そのものを入力として受け取らず、アプリが定めた条件へ値だけを入れる方法を説明します。
1同じ二人のレコードを準備する
nameまたはuidを使ってaliceを検索します。
from sd10 import *
lab = Search()
print(lab.query文書のnameを検索する("alice"))
print(lab.directoryLDAPのuidフィルターを評価する("alice"))どちらもaliceだけです。以後も同じ二人の保存資料を使い、検索に渡す値を変えます。
2文字列の代わりに演算子を渡す
文書検索のnameへ、文字列ではなく$neを持つ辞書を渡します。
value = {"$ne":None}不一致の演算子を値の位置へ渡す
print({"name":value})
print(lab.query(value))nameがnullでないという条件となり、aliceとbobが返ります。外側のフィールド名を固定しただけでは、内側へ渡した演算子までデータ扱いになるとは限りません。
3許可された型へ限定する
この機能の入力は利用者名の文字列だけです。
print(lab.typed入力型を検査してから検索する(value))
print(lab.typed("alice"))
print(lab.typed('{"$ne":null}'))辞書はtype_rejected、aliceは1件、JSONに見える文字列は0件です。文字列をさらにJSONへ再解釈していません。
4LDAPの星印が構文になる例を見る
uidへ*を直接連結します。
print(lab.directory("*")特殊文字を直接フィルターへ連結する)(uid=*)はuid属性の存在を調べるフィルターで、二人が返ります。入力文字の星印を名前の一部として探した結果ではありません。
5LDAPの値用エスケープを適用する
同じ星印をescape_filter_charsへ通します。
print(escape_filter_charsLDAPフィルター値の特殊文字を符号化する("*"))
print(lab.directory("*",escaped=True符号化した値を同じ検索へ渡す))星印は\2aとなり、(uid=\2a)は文字としての*を探すため0件です。HTMLやSQL用のエスケープではなく、LDAPフィルター値の規則を使います。
6括弧・バックスラッシュ・NULも処理する
フィルター値で意味を持つ文字をまとめて確認します。
print(escape_filter_chars特殊文字の変換結果を確認する("a(b)*"+chr(92)+chr(0)))
print(lab.directory("alice",escaped=True通常名の検索が保たれることを確認する))括弧、星印、バックスラッシュ、NULが16進のescapeになり、通常のaliceはそのまま検索できます。DNのエスケープとは規則が異なります。
7保存データを変更していないことを確認する
検索条件の変更によって返る対象が広がっても、ここまでの操作は読取りです。
print(list(lab.docs.find元の文書を照合する({}, {"_id":0}).sort("name")))
print(lab.directory("*")固定ディレクトリの全対象を照合する)元の二人とroleが保たれています。検索結果の漏えいと保存データの改変は別の影響として評価します。
まとめ
JSONの値位置にも演算子が入る可能性を調べます。
入力型を契約で固定し、文字列を再解釈しません。
LDAPフィルターの値には専用のエスケープを使います。
操作履歴と解説を振り返れます。
Python 3.15 / mongomock 4.3.0 / ldap3 2.9.1 MOCK_SYNC
from sd10 import *
lab = Search()
print(lab.query("alice"))
print(lab.directory("alice"))['alice']
('(uid=alice)', ['alice'])value = {"$ne":None}
print({"name":value})
print(lab.query(value)){'name': {'$ne': None}}
['alice', 'bob']print(lab.typed(value))
print(lab.typed("alice"))
print(lab.typed('{"$ne":null}'))type_rejected ['alice'] []
print(lab.directory("*"))('(uid=*)', ['alice', 'bob'])print(escape_filter_chars("*"))
print(lab.directory("*",escaped=True))\2a
('(uid=\\2a)', [])print(escape_filter_chars("a(b)*"+chr(92)+chr(0)))
print(lab.directory("alice",escaped=True))a\28b\29\2a\5c\00
('(uid=alice)', ['alice'])print(list(lab.docs.find({}, {"_id":0}).sort("name")))
print(lab.directory("*"))[{'name': 'alice', 'role': 'viewer'}, {'name': 'bob', 'role': 'editor'}]
('(uid=*)', ['alice', 'bob'])演習完了