はじめに
ちゃんとなさい。1 nikkieです
先日徳丸さんによるやられサイトBad Todo Listで、SQLインジェクションしました。
これで湯婆婆に遭遇しても対抗できます🙌
Bad Todo ListはPHPで実装されているのですが、「これがPython、特にDjangoだったらどうなんだろう」と思い至り、少し手を動かしてみました。
目次
- はじめに
- 目次
- 文献調査:DjangoとSQLインジェクション
- SQLインジェクションできちゃうDjangoアプリケーションを実装(脆弱性の作り込み)
- Let's SQLインジェクション!
- 思ったこと
- 終わりに
文献調査:DjangoとSQLインジェクション
Django Congress 2019 「現場で使える Django のセキュリティ対策」
現場で使えるDjangoの教科書シリーズのakiyokoさんによる発表。
この中でSQLインジェクションについても取り上げられています2。
- ORMを使っていればSQLインジェクションは対策済み
- マネージャーのrawメソッドのような生のSQLを実行する場合、SQLインジェクションのリスクあり
『実践Django』(2021)
芝田さんによる書籍でも「2.6 SQLインジェクションによる攻撃を理解する」という節があります。
(この本、Djangoの知識にとどまらず、Web開発で注意すべき脆弱性も網羅されていて、めちゃめちゃ本格的です👏 オススメ)
SQLインジェクションされちゃう例
sql = "SELECT id, title FROM snippets WHERE id = '{}';".format(snippet_id)
Snippet.objects.raw(sql)
と実行する実装です。
ここでsnippet_id
の指す文字列が"'; DELETE FROM snippets WHERE '1' = '1"
とすると(※攻撃されている)
SELECT id, title FROM snippets WHERE id = ''; DELETE FROM snippets WHERE '1' = '1'
SELECT文とDELETE文が実行され、snippetsテーブルのレコードが全て消えます😱
SQLインジェクションできちゃうDjangoアプリケーションを実装(脆弱性の作り込み)
Bad Todo ListのSQLインジェクションサポート部分をDjangoで実装した上で攻撃してみます。
※脆弱性の理解を深めるため(=学習用)にやっているのであって、実際の開発では真似しちゃだめですからね!(真似したら脆弱性を作っちゃいます⚠️)
『実践Django』を参考に実装しました。
http://127.0.0.1:8000/todolist/ でTODOリストが見られます(Bad Todo Listと同様の例です)
SQLインジェクションの隙を作ったコードはこちら(脆弱性を埋め込んでいるので、絶対真似しないでね。使うべきはORMのfilter
メソッドです)
# views.py todo_str = request.GET["todo"] # ユーザがフォームに入力した値を受け取る sql = ( "SELECT id, id_str, todo, created_date, due_date FROM todos " "WHERE todo = '{}';".format(todo_str) ) todos = Todo.objects.raw(sql)
全容は以下にあります(手元でも動かせるかと思います)
Let's SQLインジェクション!
以下のようにフォームに入力すると、なんと複数の文が実行できちゃいます!(できちゃだめなやつ)
パソコンを買う'; SELECT id FROM todos WHERE '1' = '1
これを利用して悪いことをしていきます(グヘヘ)
※これはローカルで動かすDjangoアプリに学習目的で攻撃しているのであって、同じテンションで公開されているアプリに適用するのは攻撃になっちゃいます。
よいハッカーは公開されているアプリに対して真似しちゃぶっぶーですわ!🙅♂️
全TODOをDELETE
入力する値
パソコンを買う'; DELETE FROM todos WHERE '1' = '1'; SELECT id FROM todos WHERE '1' = '1
検索結果には1つもTodoが表示されません。
再び http://127.0.0.1:8000/todolist/ にアクセスすると、0件表示。
レコードが消えています!
なお、今回はpython manage.py loaddata
3を使ってJSONファイル(ダンプデータ)を読み込むことで、データをすぐ復旧できるようにしています。
todosテーブルをDROP
入力する値
パソコンを買う'; DROP TABLE todos; SELECT id FROM django_migrations WHERE '1' = '1
ProgrammingError
が表示されます(debug=True
にしている関係)。
http://127.0.0.1:8000/todolist/ に再度アクセスしてもProgrammingError
relation "todos" does not exist
todosテーブルが消えていますね。
入力する値の補足です。
django_migrationsはDjangoがマイグレーションに使うテーブル。
ここをtodosテーブル指定にすると、rawでのSQL実行はトランザクションのようで、DROPのあとにtodosテーブルを参照できず恐らくロールバックしていて、攻撃に失敗しました。
そこでDjango製アプリに絶対あるテーブルを指定しています。
なお、こちらの復旧は以下の手順で実施します。
python manage.py sqlmigrate todo 0001
4でtodosテーブルを作ったSQLを確認python manage.py dbshell
5からSQLを実行してテーブルを作る6- 上記の
loaddata
でデータを入れる
思ったこと
- formatメソッドで埋め込んでいるが、f-string7だと多少耐性があるかも
>>> todo_str = "パソコンを買う'; DELETE FROM todos WHERE '1' = '1'; SELECT id FROM todos WHERE '1' = '1" >>> "WHERE todo = '{}';".format(todo_str) # 複数の文になってSQLインジェクションされる "WHERE todo = 'パソコンを買う'; DELETE FROM todos WHERE '1' = '1'; SELECT id FROM todos WHERE '1' = '1';" >>> f"WHERE todo = {todo_str!r};" # formatメソッドと違って入力値の中のクオートがエスケープされる 'WHERE todo = "パソコンを買う\'; DELETE FROM todos WHERE \'1\' = \'1\'; SELECT id FROM todos WHERE \'1\' = \'1";'
- これも本質ではないのですが、Djangoモデルの
Meta
クラスでtodosというテーブル名を指定している点- https://docs.djangoproject.com/ja/4.2/topics/db/sql/#executing-raw-queries
- 指定しなければ「アプリケーション名_モデル名」
- todosと指定してもしなくても総当たりで攻撃されるのだと思います
終わりに
DjangoでSQLインジェクションされちゃうWebアプリはどう実装するんだろうという疑問を元に手を動かしました。
- マネージャのrawメソッドを使うとSQLインジェクションのリスクあり
- ユーザ入力値をformatする実装(複数文のSQLをユーザが入力できてしまい、SQLインジェクション!)
- DjangoのORMを使いましょう(SQLインジェクションの心配はなし)
- このエントリで紹介した実装は真似しないでくださいね
- またフォームに入力した値は、本番稼働しているアプリに適用しないでくださいね(それは攻撃になっちゃうぞ❤️)
今回のような危ないコードを実装していることはBanditを使えば気づけると聞いたことがあります8。
実際BanditのリポジトリにはDjangoの例があります。
https://github.com/PyCQA/bandit/blob/1.7.5/examples/django_sql_injection_raw.py
Banditは今後の素振り材料ですね。
- ダイヤちゃんさんの言! 作業BGMはAbema TVで一挙配信されていたラ!サ!!でした↩
- アーカイブは21:00くらいから ↩
- https://docs.djangoproject.com/ja/4.2/ref/django-admin/#loaddata↩
- https://docs.djangoproject.com/ja/4.2/ref/django-admin/#sqlmigrate↩
- https://docs.djangoproject.com/ja/4.2/ref/django-admin/#dbshell↩
-
PostgreSQLは
CREATE TABLE todos
するとtodos_id_seq
というシーケンスも一緒にできるのですね(シーケンスってなんなんだろう...)↩ - 『Effective Python 第2版』でもオススメ「項目4 Cスタイルフォーマット文字列とstr.formatは使わずf 文字列で埋め込む」↩
- PyCon Kyushu 2022 Kumamotoにて(スライド62/91。アーカイブは22:25から) ↩