Railsのセキュリティ脅威に立ち向かう:認証と機密データ露出への対策
本シリーズの第1回では、インジェクション攻撃について解説しました。
OWASP Top 10 Webアプリケーションセキュリティリスクをテーマにした本シリーズの第2回となる今回は、認証の不備と機密データの露出という脅威の世界を深く掘り下げていきます。
具体的には、ハッカーがどれほど簡単にあなたが書いたコードを欺き、ユーザーのデータを盗み出すことができるのか、以下のような攻撃手法を見ていきます。
- ユーザー列挙(User Enumeration):ログインページを悪用し、候補となるユーザー名のリストを総当たりで試して、データベース内にそのユーザーが存在するかどうかを確認する攻撃。
- 弱いパスワード:システムが弱いパスワードを許容している場合、ハッカーは総当たり(ブルートフォース)攻撃でユーザーのパスワードを推測できる。
- 制限されていないCookie:システムが適切なセキュリティ設定なしで機密データをCookieに保存している場合、XSS攻撃によって情報を盗まれる可能性がある。
さらに、十分に保護されていない機密データがもたらす脆弱性についても詳しく取り上げます。
- 安全でない機密データの保存:MD5のような脆弱なアルゴリズムで機密データを暗号化・ハッシュ化しているケース。
- 機密データの露出:開発者が意図せず、URLやhiddenフィールドなどに暗号化されていない機密データを公開してしまうケース。
前回の記事と同様に、今回もRailsGoatというサンプルアプリケーションを使って、これらの脅威を実践的に検証していきます。初めての方は、まず前回の記事を参照してRailsGoatのセットアップと起動を行ってください。
それでは始めましょう!
認証をめぐる脅威
現代のアプリケーション開発において、認証は欠かせない存在です。バックエンドAPIであれフロントエンドのフォームであれ、認証はセキュリティ対策の境界線を決める最も重要な工程の一つです。
重要なのは認証そのものだけではありません。その先にあるセッション管理も同様です。パスワードやトークンはどこに保存するのか? 適切に暗号化されているか? 信頼できるアルゴリズムを使っているか? パスワードは十分に複雑か?
注意すべきポイントは数多くあります。ここからは、Railsアプリケーションにおける認証とセッション管理に関わる典型的な攻撃を一つずつ見ていきましょう。
ユーザー列挙(User Enumeration)
ユーザー列挙は、攻撃者が総当たりの手法を使って「特定のデータがシステム内に存在するか」を確認するために用いる、よく知られたテクニックです。
この攻撃の最も有名な例の一つが、現在のFacebookのログインページです。以下の画像のように、無効なランダムな認証情報を入力すると、Facebookはリクエストを処理し、データベース内にそのユーザーが存在するかどうかを確認します。

Facebookのログインページ。
Log Inボタンをクリックすると、Facebookは「入力されたユーザー名(メールアドレスまたは電話番号)は無効です」というエラーメッセージを返します。

無効なユーザー名を示すメッセージ。
つまり攻撃者は、「アプリケーションがそのユーザーが登録済みかどうかを無料で教えてくれる」ということを知ります。
さらに、攻撃者がメールアドレスのリスト(ランダムに生成したものでも、どこかで購入したものでも)を持っていれば、今度はパスワードが正しいかどうかも確認できます。

無効なパスワードを示すメッセージ。
攻撃者がシステムの各バリデーションごとの応答の違いを把握できれば、存在するユーザーのリストと一般的・脆弱なパスワードのリストを作成できます。そして、アクセスに成功するまで繰り返し総当たり攻撃を仕掛けるのです。
もちろんFacebookの開発者たちもこの問題を把握しており、そのため非表示のCAPTCHAや、特定のIPアドレスからのリクエスト数に対する制限といった追加の保護策を実装しています。
ユーザー列挙を防ぐために私たちができることの一つは、ユーザー名とパスワードをまとめて検証し、汎用的なメッセージを返すことです。実際に試してみましょう。
脅威を実践で確認する
RailsGoatアプリでapp/modelsフォルダ内のuser.rbファイルを開き、authenticateメソッドを見つけてください。そこには次のコードがあります。
raise "#{email} doesn't exist!" if !(user)
if user.password == Digest::MD5.hexdigest(password)
auth = user
else
raise "Incorrect Password!"
end
お分かりですね。このメッセージの設定方法では、攻撃者に「ユーザーが存在しない」のか「パスワードが間違っている」のかを教えてしまうのです。
実際に試してみましょう。RailsGoatのログインページで適当なメールアドレスとパスワードを入力すると、次のエラーメッセージが表示されます。

ユーザーが存在しないことを示すエラー。
一方、ユーザーが存在する(例:ken@metacorp.com)のにパスワードが間違っている場合は、次のメッセージが表示されます。

パスワードが誤っていることを示すエラー。
仮にあなたのアプリが強力なパスワードのみを許可していたとしても、攻撃者は列挙した有効な顧客メールアドレスのリストを作成し、それらを標的としたフィッシングメールを送ることができます。まるであなた自身が悪意ある操作を依頼しているかのように見せかけて。
この問題の解決策
最も手軽な対策は、表示されるメッセージを変更し、ハッカーの手を煩わせることです。
app/controllersフォルダ内のsessions_controller.rbを開き、createメソッド内の次のコードを:
flash[:error] = e.message
次のように変更します。
flash[:error] = "Your credentials aren't valid."
これで、ユーザー名かパスワードのどちらを間違えても、常に同じメッセージが表示されるようになります。

無効な認証情報であることを示すメッセージ。
もう一つの方法として、user.rbモデル内の2つのメッセージ自体を変更するやり方もあります。
弱いパスワード
何度強調しても足りません。強力なパスワードの入力をユーザーに求め、そのパスワードが基準を満たしているかをチェックするバリデーションコードを必ず実装してください。
これは、ユーザー列挙を防ぐための最も重要なステップの一つです。
脅威を実践で確認する
RailsGoatでuser.rbファイルを開き、クラス定義の直前にある最初のコード行を見てください。
validates :password,
presence: true,
confirmation: true,
length: {
within: 6..40
},
...
これはパスワードが弱く検証されている典型例です。長さしかチェックしていないのが分かります。
この問題の解決策
解決策はシンプルです。より厳しい要件でパスワードを検証しましょう。例えば以下のような条件です。
- 小文字と大文字をそれぞれ最低1文字含む
- 数字を最低1つ含む
- 10文字以上である
要件を追加すればするほどパスワードは安全になります。ただし、やりすぎには注意が必要です。要件が厳しすぎると、ユーザビリティやパスワード復旧フローの複雑さが増してしまいます。
RailsGoatで対応するには、lengthプロパティを次のように置き換えるだけです。
:format => {:with => /\A.*(?=.*[a-zA-Z])(?=.*[0-9])(?=.{10}).*\z/},
その後、サインアップページで各項目を入力し、弱いパスワードを設定してみましょう。送信すると、次のエラーメッセージが表示されます。

パスワードが無効であることを示すエラー。
制限されていないCookie
前回の記事では、XSS攻撃がどのように発生するかを理解する時間を設けました。XSSは攻撃者に悪意あるスクリプトの実行を許してしまう攻撃であり、JavaScriptコード内のdocument.cookieのような属性へのアクセスを防がない限り、セッションCookieから重要な情報を盗み出される恐れがあります。
WebストレージとCookieのどちらを使うべきかという議論は、コミュニティでよく話題になります。Webストレージは扱いやすい反面、XSSからの適切な保護がない場合、攻撃者はそこに保存されたオブジェクトへ完全にアクセスできてしまいます。
一方、Cookieは正しい設定を行えば比較的安全です。例えばHttpOnlyフラグの設定などです。
簡単に言えば、セッション情報を持つCookieにHttpOnlyフラグが設定されている場合、そのCookieはJavaScriptのDocument.cookie APIからはアクセスできなくなります。こうすることで、サーバーだけがそのCookieを受け取れるようになります。
さらに、Secure属性もあります。これは、リクエストがHTTPS経由の場合にのみ(HTTPでは決して)Cookieをサーバーに送信することを保証します。これにより、中間者(man-in-the-middle)による通信の盗み見があっても、リクエストの安全性が高まります。
脅威を実践で確認する
Railsは自動的にすべてのCookieにHttpOnlyフラグを設定してくれます。これは素晴らしいことで、セキュリティに無自覚な開発者のアプリがハッキングされるのを防いでくれます。
この例をテストするには、この機能を無効化する必要があります。RailsGoatでは、config/initializersフォルダ内のsession_store.rbファイルで明示的に無効化されています。確認してみてください。
その後、再び登録ページに移動し、各項目を適切に入力して、Nameフィールドに次の内容を入力します。
<script>
alert(document.cookie);
</script>
フォームを送信するとユーザーが作成され、続いて次のアラートメッセージが表示されます。

RailsGoatのセッションCookieを暴露するアラートメッセージ。
この問題の解決策
この場合の対策は非常にシンプルです。RailsアプリでHttpOnlyフラグを無効化しないようにするだけでOKです。
httponly: falseの設定を削除してサーバーを再起動してください。同じ操作を試みると、今度は次のような空のアラートが表示されます。

中身が空のアラートメッセージ。
その他のシナリオ
図書館の共用PCやネットカフェなど、セキュリティが確保されていないコンピュータからWebアプリケーションにアクセスすることを想像してみてください。アプリケーションが一定期間の非アクティブ状態の後に自動ログアウトするよう設定されていない場合、あなたのセッションはまだ生きたまま残っています。
ブラウザのタブを閉じただけでは、アプリケーションからログアウトしたことにはなりません。
開発者のコードによく見られるもう一つの問題は、フロントエンドであらゆる種類のIDを露出させてしまうことです。ユーザーのIDをhidden inputやURLに保持しておけば、サーバーへの後続のリクエスト処理が楽になる、というシナリオは簡単に思い浮かぶでしょう。
しかし、それは窃取攻撃への半分近くを道を開いているようなものです。
機密データの露出
機密情報の安全確保に十分な労力を割くべきだという点において、このトピックは最も過小評価されているテーマの一つかもしれません。
データが常時転送中であれ保存中(at rest)であれ、通常のデータと機密データを区別することが極めて重要です。機密データには、クレジットカード番号やセキュリティコード、パスワード、個人識別番号、あるいはコンプライアンスやプライバシー関連法規(EUのGDPRやPCIなど)に関わるあらゆる情報が含まれます。
それに加え、アプリケーションが属するビジネス分野によっては、他のコンプライアンス規則が適用されるかどうか、各国の法令を確認することも重要です。
この脆弱性の典型的な例をいくつか挙げます。
- データは暗号化されているか? それとも平文のままWeb上を転送されているか?
- 暗号化しているなら、どんなアルゴリズムを使っているか? 最新の暗号解読攻撃に対して強固で信頼できるものか?
- 鍵が指定されていない場合に、デフォルトの暗号鍵を使用していないか?
- HTTPリクエストが適切なセキュリティヘッダーによって保護されているか確認したか?
それでは、最も一般的なシナリオを分析してみましょう。
安全でない機密データの保存
Webアプリケーションの開発経験があれば、MD5メッセージダイジェストアルゴリズムについて聞いたことがある(あるいは使ったことがある)でしょう。今でもパスワードのハッシュ化に広く使われていますが、非常に脆弱であることが証明されています。
ここで重要なのは、ハッシュ化と暗号化の違いを理解することです。暗号化とは、何らかの鍵を使ってデータを復号できる状態にすることです。一方ハッシュ化は変換手法を指し、データをハッシュ値に変換することはできても、その逆はできません。
これはあらゆる種類の機密情報に当てはまりますが、MD5は主にパスワードのハッシュ化で使われることで知られています。例えば、アプリケーションがユーザーのマイナンバーなどの個人情報を扱うのであれば、データベースに安全に格納するだけでなく、アプリケーション内でデータが他のデータベースへ、特にブラウザへどのように伝送されるかにも注意を払う必要があります。
脅威を実践で確認する
これまで見てきた通り、RailsGoatは意図的にユーザーのパスワードをMD5ハッシュとして保存しています。user.rbモデル内で確認できます。
def hash_password
if self.password.present?
self.password = Digest::MD5.hexdigest(password)
end
end
ユーザーがログインするたびに、アプリは入力されたパスワードをハッシュ化し、結果が一致するかをチェックします。一致しなければパスワードが違うということになり、エラーが投げられます。
この問題の解決策
この問題への対処法は多数ありますが、最も有名なのはBCryptが提供するようなソルト付きハッシュを使う方法でしょう。
RailsにはBcryptを扱うデフォルト機能が備わっていますが、この目的で広く使われている有名なライブラリとしてbcrypt-rubyがあります。
gem install bcrypt
モデルに組み込むと、パスワードの設定方法とデータベースからの取得方法が定義され、その他はすべてライブラリが自動的に処理してくれます。
require 'bcrypt'
class User < ActiveRecord
include BCrypt
def password
@password ||= Password.new(password_hash)
end
def password=(new_password)
@password = Password.create(new_password)
self.password_hash = @password
end
end
ただし、このフローではBCryptアルゴリズムのパスワードハッシュを保存するための追加の文字列カラム(password_hash)がテーブルに必要です。
こうすることで、パスワードの値を直接モデルオブジェクトにセットでき、BCryptが安全なハッシュ化を担当してくれます。ユーザーの入力と照合するためにパスワードを取得する際も同様です。
公開しすぎているデータ
REST APIであれGraphQLエンドポイントであれ、クライアントの動作に必要な最小限のデータだけを返すようにしましょう。
JavaScriptクライアントがAPIから情報を取得し、その一部しか使わなかったとしても、攻撃者がそのエンドポイントを奪って再度呼び出し、レスポンス全体を取得したり、プロキシツールで盗み見たりするのを妨げることはできません。
常にAPIを見直し、たとえ機密情報を返す場合でも、適切な暗号化と適切な場所でのみ行われるようにしましょう。
脅威を実践で確認する
ユーザーデータを扱う際には、機密情報が漏洩しないことを保証する安全な仕組みを作ることが重要です。
api/v1フォルダ内のusers_controller.rbファイルを開いてください。そこに次のメソッドがあります。
def show
respond_with @user.as_json
end
非常にシンプルですが、Webクライアントからこのエンドポイントにアクセスすると、レスポンスにはパスワードを含むユーザーの全フィールドが含まれてしまいます。
userモデルだけでなく、機密情報を保持するすべてのモデルに対して、APIに公開する属性を選択する仕組みが必要です。
この問題の解決策
幸い、Railsにはこれに対処する非常に簡単な方法が用意されています。as_jsonメソッドを次のようにオーバーライドしましょう。
def as_json
super(only: [:id, :email])
end
これで、デフォルトですべてを公開するのではなく、必要なデータだけを返すようになります。各モデルについて重要なフィールドを選択し、同じ考え方を適用してください。
まとめ
今回は、認証の不備と機密データ露出という領域を航海してきました。これらのルールに従えば、ユーザーと顧客にとってはるかに安全なアプリケーションを保証できるでしょう。
加えて、Ruby on Rails公式のセキュリティドキュメントに目を通すことの重要性はいくら強調しても足りません。そこでは、セッションハイジャック、ストレージの仕組み、データを最良の形で暗号化する戦略など、さらなる情報を見つけることができます。
それでは、次の記事でお会いしましょう!
-
マルウェアやセキュリティの脅威からPCとデータを守る方法
マルウェアや個人情報の盗難は、PCユーザーにとって最も深刻な悩みの一つです。あまりの恐怖に、PCの使用そのものをやめてしまったというユーザーもいるほどです。しかし、コンピュータを使わないことやオフラインで過ごすことは解決策ではなく、単なる問題からの逃避にすぎません。そこで本記事では、これらの問題を詳しく解説し、マルウェア対策やPC上のデータ・情報への脅威への対処法について学んでいきます。 マルウェアとは? 悪意のある目的で開発されたソフトウェア全般を「マルウェア」と呼びます。攻撃手法の違いにより、ウイルス、トロイの木馬、ランサムウェア、スパイウェアなどさまざまな種類が存在します。中にはPCに
-
DUアンチウイルスアプリがユーザーデータを無断収集——Google Playからの削除と復帰の全経緯
人気モバイルセキュリティアプリ「DU Antivirus Security」が、Google Playストアに復帰しました。一度は削除されたこのアプリは、なぜ再び公開されることになったのでしょうか。まずは全体の経緯をご説明します。DU Antivirus Securityは、最も利用者の多いモバイル向けアンチウイルスアプリの一つでした。しかし、セキュリティ企業Check Pointが、このアプリがユーザーのスマートフォンから密かにデバイスデータを収集していたことを明らかにした結果、アプリストアから削除される事態となりました。DU Antivirus SecurityはDU Groupによって開