Tuesday, December 31, 2013

JNDI で LDAP インジェクション対策のささやかなこころみ

SQL に 「SQL インジェクション」 があるように, LDAP にも 「LDAP インジェクション」 というものがあるようです。 ただ, ネット上で調べても, SQL に比べると LDAP の事例はあまり多くは見かけない印象があります。 LDAP を利用するアプリケーションが世の中にそれほど多くないから, ということだけではなく, LDAP で利用する BIND や SEARCH や MODIFY などの構文は, 単なる文字列の組み立てだけで書くことができるわけではないから, ということも理由なのかなという気がしています。 とはいえ, 文字列の組み立てが発生しないわけでもないので, 無関心でいるよりは, 多少なりとも関心を持っておきたいとも思います。

たぶん, LDAP インジェクションのおもなものは, DN や RDN の文字列だとか, 検索フィルターの文字列だとかを動的に組み立てる場面において特に多いのだろうと思います。 ほかには, 属性名とか属性値とかを動的に組み立てる場面に置いても, ありうるかもしれません。 これらのうち, 検索フィルターの文字列に関して, 少しだけメモを残しておきたいと思います。

認証を SEARCH オペレーションでおこなう場合

ユーザーの認証に必要となる情報がバックエンドの LDAP サーバーに格納してあるような業務環境において, アプリケーションにログインしてくるユーザーを認証するにあたり, アプリケーションから LDAP サーバーに対して SEARCH をおこない, 合致するものがあれば 「認証成功」 とみなす。 こんなアプリケーションは少なくないようです。 せっかくディレクトリサービスを利用するんだから, SEARCH じゃなくて BIND を使えばいいのに... とは思うのですが, どうもリレーショナルデータベースにユーザー情報を格納している場合と同じような感覚でそのようにしている例はしばしば見受けられます。 まあ, それはそれでもいいんですが。

たとえば, ユーザーが入力してきた ID とパスワードをもとに, 次のように検索フィルターの文字列を組み立てて SEARCH をおこなうとします。

        String filter = String.format("(&(uid=%s)(userPassword=%s))",
                userId, password);

ここで, たとえば ID が "alice" で, パスワードが "wonderland" であれば, できあがる検索フィルターは次のようになります。

        (&(uid=alice)(userPassword=wonderland))

ちなみに, LDAP サーバーのデータベースにパスワードをクリアテキストで保存するというのは考えづらくて, その時点でこの例はすでにあまり現実的ではないのですが, とりあえずその点は脇に置かせていただきます。

ここで, もし ID に "alice)(|(objectClass=*" を, パスワードに "1234)" を与えると, できあがる検索フィルターは次のようになります。

        (&(uid=alice)(|(objectClass=*)(userPassword=1234)))

どんなオブジェクトにも objectClass 属性はありますから, あとはアリスさんの ID が "alice" でありさえすれば, この検索フィルターを用いた SEARCH はアリスさんのユーザー情報を格納したオブジェクトにヒットします。 つまり, 攻撃者さんは, アリスさんのパスワードを知らなくても, アリスさんの ID さえ知っていれば, アリスさんのふりをして認証を受けることが可能となってしまいます。

これに対しては, ユーザーが入力してきた ID とパスワードに '(' や ')' や '*' といったメタ文字が含まれているのがいけないのだから, これらを禁止したりエスケープしたりすれば済む話ではないか, というのが一般的な解かもしれません。 もし SQL インジェクション対策という文脈であれば, プレースホルダーを設けたプリペアードステートメントを使うという方法を検討することもできるのですが, LDAP 検索における検索フィルターに関してはそういうものがありませんので, たしかに禁止するなりエスケープするなりということを考えなければなりません。

面倒くさいなあと思ってしまうところですけれど, 幸い JNDI には, 意図してか意図せずしてか, アプリケーションの開発者の代わりにメタ文字のエスケープをしてくれる API がいくつかあります。 もちろん, これらは, リレーショナルデータベースにおけるプリペアードステートメントとは違って, すなわちサーバー側 (LDAP サーバー) がうまいことやってくれるわけではなくて, クライアント側 (アプリケーションから利用するライブラリなど) が何とか気を利かせてくれるというようなものですので, エスケープ大好き and/or プレースホルダー大嫌いの方々からは, そんなものに頼るのは教育上いくない! ライブラリに重大な欠陥があったらどうするんだ! と非難されそうではあります。 でも, そもそもアプリケーションが本来責任を負うべきビジネスロジックから外れたことはできるだけそれぞれに特化したツールなどに任せるようにしたほうが結果的には不具合や脆弱性の少ないものを作り上げるのに資するとは思うのですよね。 大きなミスを防ぐには, 餅は餅屋で, 役割分担がいいと思っています。

そういうわけで, 先ほどの JNDI の API を利用する場合の例を, ふたつだけ書きとめておきたいと思います。

Attributes を使う

次のようなコードを考えることができます。

        Attributes attrs = new BasicAttributes();
        attrs.put("uid", userId);
        attrs.put("userPassword", password);
        NamingEnumeration<SearchResult> results
                = ldapContext.search("dc=localdomain", attrs);

Attributes が, プレースホルダー代わりになってくれます。 ここで, もし ID やパスワードに先ほどの例のようなメタ文字を混入させても, search メソッドはそれらに対して必要最小限のエスケープを施してくれます。 その結果, 検索フィルターは次のようになります。

        (&(uid=alice\29\28|\28objectClass=\2A)(userPassword=1234\29))

ただし, この方法の弱点は, search メソッドの引数に SearchControls を渡すことができず, そのため検索スコープが常に ONELEVEL_SCOPE (one; singleLevel) となってしまう点にあります。 ou などが多重に入れ子になっているような場合は, やや面倒くさいかもしれません。

filter expression を使う

次のようなコードを考えることができます。

        String expr = "(&(uid={0})(userPassword={1}))";
        Object[] args = new Object[] { userId, password };
        SearchControls cons = new SearchControls();
        cons.setSearchScope(SearchControls.SUBTREE_SCOPE);
        NamingEnumeration<SearchResult> results
                = ldapContext.search("dc=localdomain", expr, args, cons);

search メソッドの第二引数 (filter expression) がプレースホルダー代わりとなり, 第三引数が各パラメーターに充当されることになります。 ここで, もし ID やパスワードに先ほどの例のようなメタ文字を混入させても, やはり次のようにエスケープしてくれます。

        (&(uid=alice\29\28|\28objectClass=\2A)(userPassword=1234\29))

この方法は, 検索スコープを指定することができますので, Attributes を使う方法よりも便利かもしれません。 また, AND 条件だけでなく OR 条件も必要だとか, '=' 以外の比較演算子を使いたいだとか, そういった Attributes には荷が重い場面でも, 気軽に使うことのできる方法であるとも言えそうです。

まとめ

実は, ネット上で見かける LDAP インジェクションが発生しうるパターンのいくつかについては, まだよく理解が及んでいないところが少なくなくて, これからもっと調べなければいけないことはたくさんありそうな気がするのですが, そろそろ今年も終わりですので, 今夜はここまでにしたいと思います。 よいお年を。

No comments:

Post a Comment