Android のデータベースといえば、SQLite ですが、大抵の場合、ContentProvider によって CRUD がラップされ、ContentResolver 経由でデータソースにアクセスすることになります。
この設計によって、モデル内のデータソースが何であれ、CRUD のインタフェースが統合されるので、データソースのレイヤがどうなっているかをデータ利用者側が意識する必要がないようになっています。
が、そのインタフェースはシンプル故に、凝ったことをしようとすると、ContentProvider が頑張るか、利用者が頑張るかしないといけなくなります。
特に、データソースが SQLite の場合、SQL なら便利に出来ていたことが途端に難しくなります。例えば、行データの重複を消したり、グループ化したり、など。
Cursor からデータを取り出して、自分たちで同じような機能を実現することもできますが、できることなら SQL で実現したほうがいいですよね。
さてここで、ContentResolver#query()
のprojection
やselection
が文字列配列や文字列で渡せることを思い出してください。
文字列で渡せるということは、好きな SQL をそこに書くこともできる、ということです。実際、システムでは、以下のテンプレート文字列に合わせて SQL が構築されるため、比較的容易に好きな SQL を注入することができます。まさに SQL Injection そのものですね。
SELECT %s FROM %s WHERE (%s)
例えば、GROUP BY
を使いたければ、以下のように問い合わせることで実現できる可能性があります。
Cursor c = resolver.query(CONTENT_URI, null, "1) GROUP BY (some_column", null, null);
こうすると、例えば、以下のように SQL が構築されれば成功ですね。
SELECT * FROM some_table WHERE (1) GROUP BY (some_column)
WHERE 句は常に TRUE です。そこに閉じカッコからの GROUP BY 句を挟むことで、GROUP BY が実現できます。
実際、このテクニックは、ギャラリーアプリで使われていて、このコードのL64あたりに書いてあります。
Sms も同様に、Telephony.Sms
の CONTENT_URI に対して、上記のような SQL を発行することができます。
ただし、ContentProvider の実装によっては、うまくいかないものもあります。
たとえば、CallLog.Calls、これは、VoiceMail に関連するデータも含まれており、パーミッションなしにそのデータへのアクセスを許可しないようにしているため、パーミッションが無い場合は自動で WHERE 句に絞込の条件文が付与されるようになっています。そのような ContentProvider に対して、先のような SQL Injection を試すと、見事に文法が崩れてエラーとなります。
まあ、SQL Injection ですしおすし…
もし、DISTINCT したいのであれば、projection の配列を以下のようにすれば DISTINCT できますね。
final String[] projection = new String[] { "DISTINCT hoge" };
ただし、selectionArgs に SQL を埋め込んでもシステム側でよしなに対策されて無害化されるので、こちらはハックできないようですね。
まさかそんな実装にすることは無いと思いますが、以下のようにしてしまうとガバガバになってしまうので気をつけましょう。
void someQuery(String arg1) {
Cursor c = resolver.query(CONTENT_URI, null, "some_column=" + arg1);
}
プレースホルダを使いましょう。