Celeste Engineer

Androidとか自転車とか

kyobashi.dex #3 で発表してきた

当日は参加枠でお話を聞きに行くつもりでしたが、いつの間にか資料を作って発表する流れになっていたので、急ごしらえですがMediaStoreがらみのことについて LT をしてきました。

speakerdeck.com

最近MediaStoreの、特にAudio周りを触っているのですが、以前にはImagesも触ったことがあって、相変わらず面倒くさい構造になっているな……と思っていました。データの取り出し方が分かってしまえばあとは決まり文句を打つだけで良いのですが、如何せんその取り出し方が分からないものも多く、都度調べ物をしていては時間が無限にあっても足りないと感じたので、Facade のようなまとまった API をライブラリとして提供しています、というのがおおまかな概要になります。

MediaStoreそのものは単にデータベースの構成やUriを外部に公開するためのもので、その実態であるところのContentProviderは別のところにあります。開発者は通常のContentProviderへのアクセスと同じ手段でもってMediaStoreからデータを読みだすのですが、いくつかのややこしい話があります。

  1. 基本的に Javadoc が不足気味である。
  2. カラム情報を持つクラスに継承構造があり、テーブルごとのカラム情報を持つクラスは共通する親クラスのカラム情報を受け継いでいる。但し共通カラムが必ずしも各テーブルに存在するとは限らない。
  3. カラムの制約が明記されていないものがある(UNIQUE や NOT NULL など)。
  4. クエリのためのUriが正しくないと、欲しいデータが揃わない(おそらく内部的に結合を行っている)。
  5. 公開されていないUriを使って得られるデータもある。
  6. テーブルの構造的問題とContentResolverAPI の問題で、SQL インジェクションをする場面がある

最後の SQL インジェクションに関しては、4.x の時代にバンドルされていたギャラリーアプリが実践しています。ギャラリーアプリでは写真をディレクトリ単位で管理していましたが、どの写真がどのディレクトリに属しているかという情報は正規化されておらず、写真データのカラムに結合したままになっています。このため、存在するディレクトリを引いてくるには、写真データの入っているテーブルに対し、GROUP BY を伴ったクエリを発行するひつようがありますが、ContentResolverにはそのようなメソッドが生えていないため、WHERE句に相当する部分で SQL インジェクションを余儀なくされる、といった具合です。

ContentResolver#query()の引数であるselectionWHERE句を構成する部分にあたりますが、当然条件を書く上ではプレースホルダを使用します。これによって、selectionArgsに対しての SQL インジェクションは成功しないのですが、selectionそのものに SQL インジェクションをする文を書くことはできてしまいます。内部的には、selectionWHERE (%s)という文字列のテンプレートに流し込んでいるだけなので、例えば1) GROUP BY ... などとすれば、WHERE句はつねに真となって SQL インジェクションができます。

まさか公式にそれを逆手に取った実装をしているとは思いませんでしたが、まあ、そういうことです。

ContentProvider での実装如何では SQL インジェクションが容易にできる場合もあれば困難を極める場合もありますが、このあたりは(本題からはそれていますが)気をつけておくべきと言えます。まあ、Webサービスを作る場合と同じ考え方で作っていれば、このような穴が空くようには思えませんが…

それはともかくとしても、バッドノウハウとボイラプレートの山をこれ以上積み上げないためにも、また API を整理するためにも、何かしら Facade のようなものを用意して置く必要性を感じた、ということですね。

一応 GitHub には既に公開していますが、まだ mavenCentral にはリリースしていません。もうすこしやりたいことがあるので、そのめどがついたタイミングでリリースしようと思います。

GitHub - Drivemode/MediaFacade: Facade modules for dealing with complicated MediaStore.