Slide 1

Slide 1 text

ActiveRecord SQLインジェクション クイズ (Rails 7.1.3.4) 2024.10.26 Kaigi on Rails 2024 Koji NAKAMURA (@kozy4324)

Slide 2

Slide 2 text

こんな経験ありませんか?

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

order メソッドにユーザー入力を そのまま受け付けていいんだっけ?

Slide 5

Slide 5 text

ゴール もうコワクナイ、SQL インジェクション! ● SQL インジェクションの押さえるべき基本を知る ● Rails における正しい防ぎ方を知る

Slide 6

Slide 6 text

アジェンダ ● 自己紹介 ● SQL インジェクション脆弱性とその一般的な対策 ● SQL インジェクションクイズ ● Brakeman のクイズ回答 ● おわりに

Slide 7

Slide 7 text

自己紹介

Slide 8

Slide 8 text

Koji NAKAMURA ● 𝕏: @kozy4324 ● GitHub:@kozy4324 ● Classi株式会社所属 ● Shinjuku.rb所属 ● Kashiwa.rb主催 自己紹介

Slide 9

Slide 9 text

https://corp.classi.jp/careers/ 「Classi」と「tetoru」は Ruby on Rails で開発されています

Slide 10

Slide 10 text

https://shinjukurb.connpass.com/event/324989/ 今回 Shinjuku.rb で書いたプロポーザルが採択されました!

Slide 11

Slide 11 text

https://kashiwarb.connpass.com/ Kashiwa.rb を立ち上げました。こちらもよろしくお願いします!

Slide 12

Slide 12 text

SQLインジェクション脆弱性と その一般的な対策

Slide 13

Slide 13 text

SQLインジェクション脆弱性とは データベースと連携したウェブアプリケーションの多くは、利用者からの入 力情報を基に SQL 文(データベースへの命令文)を組み立てています。こ こで、SQL 文の組み立て方法に問題がある場合、攻撃によってデータベー スの不正利用を招く可能性があります。このような問題を「SQL インジェク ションの脆弱性」と呼び、問題を悪用した攻撃を、「SQL インジェクション攻 撃」と呼びます。 安全なウェブサイトの作り方 改訂第7版 https://www.ipa.go.jp/security/vuln/websecurity/about.html

Slide 14

Slide 14 text

Rails で問題になる SQL 文の組み立て方法 上の危険なコードが検索用のアクションにあり、ユーザーは検索したいプロジェクト名を入力できると します。ここで、悪意のあるユーザーが ' OR 1) -- という文字列を入力すると、以下の SQLクエ リが生成されます。 2つのダッシュ「--」が末尾に置かれると、以後に追加されるクエリがすべてコメントと見なされてしま い、実行されなくなります。そのため、 projectsテーブルからすべてのレコードが取り出されます。 Rails セキュリティガイド - Railsガイド https://railsguides.jp/v7.1/security.html Project.where("name = '#{params[:name]}'") SELECT * FROM projects WHERE (name = '' OR 1) --')

Slide 15

Slide 15 text

発生しうる脅威 ● データベースに蓄積された非公開情報の閲覧 個人情報の漏えい 等 ● データベースに蓄積された情報の改ざん、消去 ウェブページの改ざん、パスワード変更、システム停止 等 ● 認証回避による不正ログイン ログインした利用者に許可されている全ての操作を不正に行われる ● ストアドプロシージャ等を利用した OS コマンドの実行 システムの乗っ取り、他への攻撃の踏み台としての悪用 等 安全なウェブサイトの作り方 改訂第7版 https://www.ipa.go.jp/security/vuln/websecurity/about.html

Slide 16

Slide 16 text

☠ どれもサービス継続が困難になるレベルの脅威

Slide 17

Slide 17 text

● SQL文の組み立ては全てプレースホルダで実装する ○ 静的プレースホルダ プレースホルダのまま SQL 文をコンパイルしておき、データベースエンジン側で値を割り当てる方 式 (Prepared Statement) ○ 動的プレースホルダ アプリケーション側のデータベース接続ライブラリ内で値をエスケープ処理してプレースホルダには め込む方式 ● SQL文の組み立てを文字列連結により行う場合は、エスケープ処理等を行うデータ ベースエンジンのAPIを用いて、SQL文のリテラルを正しく構成する 安全なウェブサイトの作り方 改訂第7版 https://www.ipa.go.jp/security/vuln/websecurity/about.html 一般的な対策方法

Slide 18

Slide 18 text

ここまでのまとめ ● SQL インジェクション脆弱性とは ○ データベースの不正利用を招く可能性がある問題 ○ サービス継続が困難になるレベルの脅威 ● 一般的な対策方法は 3つ ○ 静的プレースホルダ ○ 動的プレースホルダ ○ エスケープ処理等を行うデータベースエンジンのAPIを用いて、SQL文 のリテラルを正しく構成する

Slide 19

Slide 19 text

🤔 Rails はどの対策方法をとっている? 3つの対策方法

Slide 20

Slide 20 text

SQLインジェクションクイズ

Slide 21

Slide 21 text

SQL インジェクション脆弱性があるのはどれ? 󰍹 User.find_by(name: params[:name]) 󰍽 User.where("name = #{params[:name]}") 󰍼 User.order("age #{params[:order]}") 󰍶 User.exists?(params[:id])

Slide 22

Slide 22 text

シンキングタイム 15秒 🕘

Slide 23

Slide 23 text

SQL インジェクション脆弱性があるのはどれ? 󰍹 User.find_by(name: params[:name]) 󰍽 User.where("name = #{params[:name]}") 󰍼 User.order("age #{params[:order]}") 󰍶 User.exists?(params[:id])

Slide 24

Slide 24 text

󰍹 User.find_by(name: params[:name]) 脆弱性があると思う方 🙋

Slide 25

Slide 25 text

󰍽 User.where("name = #{params[:name]}") 脆弱性があると思う方 🙋

Slide 26

Slide 26 text

󰍼 User.order("age #{params[:order]}") 脆弱性があると思う方 🙋

Slide 27

Slide 27 text

󰍶 User.exists?(params[:id]) 脆弱性があると思う方 🙋

Slide 28

Slide 28 text

順番にみていきましょう ✅

Slide 29

Slide 29 text

󰍹 User.find_by(name: params[:name])

Slide 30

Slide 30 text

👍 SQL インジェクション脆弱性の問題はない

Slide 31

Slide 31 text

👍 User.find_by(name: params[:name]) ● e.g.) postgresql ドライバの場合 ○ デフォルトでは prepared_statements: true ○ 静的プレースホルダ (prepared statement) で SQL 文が組み立てられる ● e.g.) mysql2 ドライバの場合 ○ デフォルトでは prepared_statements: false ○ リテラルはエスケープ処理されて SQL 文が組み立てられる irb(main):001> User.find_by(name: "' OR 1 --") User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."name" = $1 LIMIT $2 [["name", "' OR 1 --"], ["LIMIT", 1]] irb(main):001> User.find_by(name: "' OR 1 --") User Load (1.6ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = '\' OR 1 --' LIMIT 1 postgresql mysql2

Slide 32

Slide 32 text

󰍽 User.where("name = #{params[:name]}")

Slide 33

Slide 33 text

☠ SQL インジェクション脆弱性の問題がある

Slide 34

Slide 34 text

☠ User.where("name = #{params[:name]}") ● SQL フラグメントはそのまま SQL 文へ組み込まれる ● 位置指定ハンドラ or 名前付きハンドラを使用する必要がある ○ User.where("name = ?", params[:name]) ○ User.where("age > :age", { age: params[:age] }) irb(main):001> User.where("name = '' OR true") User Load (3.3ms) SELECT "users".* FROM "users" WHERE (name = '' OR true) /* loading for pp */ LIMIT $1 [["LIMIT", 11]] postgresql

Slide 35

Slide 35 text

🔑 安全かどうかを見極めるポイント ● SQL 文のリテラルと1対1に対応する値は原則的に安全 ○ 静的プレースホルダ (prepared statement) で適用されるか、もしくは SQL 文 のリテラルとしてエスケープできることが自明 irb(main):001> User.find_by(name: "' OR 1 --") User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."name" = $1 LIMIT $2 [["name", "' OR 1 --"], ["LIMIT", 1]] postgresql irb(main):001> User.find_by(name: "' OR 1 --") User Load (1.6ms) SELECT `users`.* FROM `users` WHERE `users`.`name` = '\' OR 1 --' LIMIT 1 mysql2 irb(main):013> User.where("name = ?", "' OR 1 --") User Load (2.4ms) SELECT "users".* FROM "users" WHERE (name = ''' OR 1 --') /* loading for pp */ LIMIT $1 [["LIMIT", 11]] postgresql

Slide 36

Slide 36 text

● SQL フラグメントにおいて、位置指定ハンドラ or 名前付きハンドラを利 用していないものは原則的に安全ではない ○ エスケープすべき部分文字列がどこからどこまでかが自明ではない ○ ActiveRecord はエスケープ処理をしない(できない) 🔑 安全かどうかを見極めるポイント irb(main):001> User.where("name = 'joe' AND age > 20") User Load (1.2ms) SELECT `users`.* FROM `users` WHERE (name = 'joe' AND age > 20) /* loading for pp */ LIMIT 11 mysql irb(main):001> User.where("name = '' OR true") User Load (3.3ms) SELECT "users".* FROM "users" WHERE (name = '' OR true) /* loading for pp */ LIMIT $1 [["LIMIT", 11]] postgresql

Slide 37

Slide 37 text

󰍼 User.order("age #{params[:order]}")

Slide 38

Slide 38 text

👍 SQL インジェクション脆弱性の問題はない

Slide 39

Slide 39 text

👍 User.order("age #{params[:order]}") ● リテラルと1対1に対応するケースではない ○ SQL フラグメントのケースのように見えるが... ● 許可された文字列パターンを満たさない場合は例外が発生する ● 例外により SQL インジェクション攻撃は成立しない irb(main):001> User.order("age desc; SELECT * FROM users;--") (irb):1:in `': Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "age desc; SELECT * FROM users;--".This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql(). (ActiveRecord::UnknownAttributeReference) postgresql

Slide 40

Slide 40 text

👍 User.order("age #{params[:order]}") ● 例えば複雑な式を使ったソートがしたい場合はどうする? ● Arel.sql() でラップすれば良い irb(main):001> User.order("updated_at - created_at desc") (irb):1:in `': Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "updated_at - created_at desc".This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql(). (ActiveRecord::UnknownAttributeReference) postgresql irb(main):002> User.order(Arel.sql("updated_at - created_at desc")) User Load (4.0ms) SELECT "users".* FROM "users" /* loading for pp */ ORDER BY updated_at - created_at desc LIMIT $1 [["LIMIT", 11]] postgresql

Slide 41

Slide 41 text

󰍶 User.exists?(params[:id])

Slide 42

Slide 42 text

☠ SQL インジェクション脆弱性の問題がある

Slide 43

Slide 43 text

● exists? は引数を受け取るバリエーションが複数ある ○ Integer - Finds the record with this primary key. ○ String - Finds the record with a primary key corresponding to this string (such as '5'). ○ Array - Finds the record that matches these where-style conditions. ○ Hash - Finds the record that matches these where-style conditions. ○ false - Returns always false. ○ No args - Returns false if the relation is empty, true otherwise. ● Array の場合は SQL フラグメントを受け付ける ○ e.g.) Person.exists?(['name LIKE ?', "%#{query}%"]) ● 一方 params[:id] はクエリストリング次第で Array の値もとる ○ ?id[]=1%3D1 → Parameters: {"id"=>["1=1"]} ☠ User.exists?(params[:id])

Slide 44

Slide 44 text

☠ User.exists?(params[:id]) class UsersController < ApplicationController def index @exists = User.exists?(params[:id]) end end app/controllers/users_controller.rb Started GET "/users/index?id[]=1%3D1" for ::1 at 2024-10-15 09:03:07 +0900 Processing by UsersController#index as HTML Parameters: {"id"=>["1=1"]} User Exists? (1.5ms) SELECT 1 AS one FROM "users" WHERE (1=1) LIMIT $1 [["LIMIT", 1]] : Completed 200 OK in 14ms (Views: 11.0ms | ActiveRecord: 1.4ms | Allocations: 1892) log/development.log

Slide 45

Slide 45 text

● SQL フラグメントを受け取るメソッドを一定把握する必要がある ○ e.g.) calculate, find_by, from, group, having, joins, select, where, etc… ○ https://rails-sqli.org/ にカタログされているので一読しておくと良い ● クエリメソッドへ外部入力(特に params )を雑に渡すな!!! ○ エスケープが確実に適用される形式を選択する ○ もしくは渡される値が制限されるような入力値チェックを行う 🔑 exists? のケースから学べること

Slide 46

Slide 46 text

SQL インジェクションクイズ結果 👍 User.find_by(name: params[:name]) ☠ User.where("name = #{params[:name]}") 👍 User.order("age #{params[:order]}") ☠ User.exists?(params[:id])

Slide 47

Slide 47 text

ActiveRecord のエスケープ処理 ● 内部では quote メソッドが呼ばれる ○ ActiveRecord::ConnectionAdapters::PostgreSQL::Quoting#quote ■ pg gem の #escape ● libpg の PQescapeStringInternal 関数 ○ ActiveRecord::ConnectionAdapters::Mysql2Adapter#quote ■ mysql2 gem の #escape ● libmysql の escape_string_for_mysql 関数 ● データベースエンジンの API を使っていることが確認できる

Slide 48

Slide 48 text

sanitize_sql_array メソッド ● 位置指定ハンドラの I/F がないメソッドの場合にどうするか? ○ e.g.) User.joins("INNER JOIN books b ON b.type = '#{type}'") ● sanitize_sql_array メソッドを使う ○ e.g.) User.joins(User.sanitize_sql_array(["INNER JOIN books b ON b.type = ?", type])) ● sanitize_sql_like など状況別に利用できる API が用意されている ○ API リファレンスを確認しておくと良い

Slide 49

Slide 49 text

1. 静的プレースホルダ 2. 動的プレースホルダ 3. エスケープ処理等を行うデータベースエンジンの API を用いて、 SQL 文を正しく構成する ● Rails 内部 (Rails → DBドライバ) において ○ 静的プレースホルダを使う設定においては「 1.」 ○ 静的プレースホルダを使わない設定や位置指定ハンドラなどにおいては「 3.」 ● Rails が提供する I/F (Rails ユーザー → Rails) において ○ 静的プレースホルダを使う設定にすることで「 1.」 ○ 位置指定ハンドラや snitize_sql_array といった I/F が提供されている「2.」 Rails はどの対策方法をとっていた?

Slide 50

Slide 50 text

💯 Rails では3つの対策方法を使い分けていた

Slide 51

Slide 51 text

● SQL フラグメントを受け取るメソッドを一定把握する ● クエリメソッドへ外部入力(特に params )を雑に渡さない ● クエリメソッドへ外部入力を渡す時は以下のいずれかを行う ○ エスケープが確実に適用される形式を選択する ○ 位置指定ハンドラ or 名前付きハンドラを利用する ○ sanitize_sql_array などの sanitize メソッドを組み合わせて利用する ○ 渡される値が制限されるような入力値チェックを行う Rails での SQL インジェクションの防ぎ方

Slide 52

Slide 52 text

Brakeman のクイズ回答

Slide 53

Slide 53 text

https://brakemanscanner.org/

Slide 54

Slide 54 text

Brakeman とは? ● Ruby on Rails Static Analysis Security Tool ● Rails 7.2 からはデフォルトで含まれる ○ rails new すると Gemfile に含まれる ● コードをスキャンして脆弱性疑いをレポートしてくれる ● 多くの種類の脆弱性をレポートしてくれる ○ e.g.) XSS, コマンドインジェクション, オープンリダイレクト, etc…

Slide 55

Slide 55 text

== Warning Types == No warnings found Brakeman のクイズ回答 1⃣ 👍 User.find_by(name: params[:name]) == Warnings == Confidence: High Category: SQL Injection Check: SQL Message: Possible SQL injection Code: User.where("name = #{params[:name]}") File: app/controllers/users_controller.rb Line: 4 2⃣ ☠ User.where("name = #{params[:name]}") == Warning Types == No warnings found 3⃣ 👍 User.order("age #{params[:order]}") == Warnings == Confidence: High Category: SQL Injection Check: SQL Message: Possible SQL injection Code: User.exists?(params[:id]) File: app/controllers/users_controller.rb Line: 6 4⃣ ☠ User.exists?(params[:id])

Slide 56

Slide 56 text

💯 全問正解

Slide 57

Slide 57 text

Brakeman はどのようにスキャンしている? ● ソースコード → AST(抽象構文木) → パターンチェック ○ Rails バージョンごとのチェック対象メソッドが網羅されている

Slide 58

Slide 58 text

🎉 SQL フラグメントを受け取るメソッドを 人間が頑張って全て覚える必要がない!

Slide 59

Slide 59 text

注意点: 偽陽性は多い ● 「非常に疑わしい (extremely suspicious) 」というスタンス ● Brakeman のレポートがオオカミ少年にならないように ○ 特定の warning を無視する設定を追加して運用することも検討する ○ 詳細はドキュメントを参考ください ■ Ignoring False Positives - https://brakemanscanner.org/docs/ignoring_false_positives/

Slide 60

Slide 60 text

おわりに

Slide 61

Slide 61 text

もうコワクナイ、SQL インジェクション! ● 一般的な対策方法 ○ 静的プレースホルダ ○ 動的プレースホルダ ○ エスケープ処理等を行うデータベースエンジンの API を用いて SQL 文を正しく構成する ● Rails における SQL インジェクションの正しい防ぎ方 ○ SQL フラグメントを受け取るメソッドを一定把握する ○ クエリメソッドへ外部入力(特に params )を雑に渡さない ○ クエリメソッドへ外部入力を渡す時は以下のいずれかを行う ■ エスケープが確実に適用される形式を選択する ■ 位置指定ハンドラ or 名前付きハンドラを利用する ■ sanitize_sql_array など適切な sanitize_sql_* メソッドを利用する ■ 渡される値が制限されるような入力値チェックを行う ● Brakeman も活用しよう! ○ 人間が頑張って覚えなければいけないことが減る

Slide 62

Slide 62 text

Appendix: 参考リンク ● 安全なウェブサイトの作り方 | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構 https://www.ipa.go.jp/security/vuln/websecurity/about.html ● Rails セキュリティガイド - Railsガイド https://railsguides.jp/v7.1/security.html ● Rails SQL Injection Examples https://rails-sqli.org/ ● Brakeman https://brakemanscanner.org/

Slide 63

Slide 63 text

EOF