はじめに
こんにちは、かるめと申します。Webアプリケーション開発について学習しています。
最近Webアプリケーションセキュリティに興味を持ち、主に書籍で学習しました。
また、私は主にRuby on Railsで開発を行っているのですが、フレームワークが内部で行っているセキュリティ対策も気になったので調べてみました。
そこで今回は、Webアプリケーションを開発する上で対策すべき主要な脆弱性と、Railsに組み込まれている対策、そして開発者が注意することをまとめました。
この記事で取り上げる脆弱性は以下になります。
- CSRF(クロスサイトリクエストフォージェリ)
- XSS(クロスサイトスクリプティング)
- SQLインジェクション
- セッションハイジャック
補足ですが、OWASP Top 10というものがあります。
OWASP Top 10 は、開発者やウェブアプリケーションセキュリティに関わる人々のための、標準的な啓発資料 (Awareness document) です。これは、ウェブアプリケーションに対する最も深刻なセキュリティリスクについて、広く認められた見解を表しています。
(公式ページより引用)
以下が今現在の最新版であるOWASP Top 10:2025のリストになります。
| 順位 | カテゴリ |
|---|---|
| A01 | アクセス制御の不備 (Broken Access Control) |
| A02 | セキュリティ設定の不備 (Security Misconfiguration) |
| A03 | ソフトウェアサプライチェーンの不備 (Software Supply Chain Failures) |
| A04 | 暗号化の不備 (Cryptographic Failures) |
| A05 | インジェクション (Injection) |
| A06 | 安全性を欠いた設計 (Insecure Design) |
| A07 | 認証の不備 (Authentication Failures) |
| A08 | ソフトウェアまたはデータの完全性の不備 (Software or Data Integrity Failures) |
| A09 | セキュリティログとアラートの不備 (Security Logging and Alerting Failures) |
| A10 | 例外的な状況への不適切な対応 (Mishandling of Exceptional Conditions) |
本記事で取り上げる脆弱性も、CSRFはA01 アクセス制御の不備に、XSSとSQLインジェクションはA05 インジェクションに、セッションハイジャックはA07 認証の不備にそれぞれ含まれます。
CSRF(クロスサイトリクエストフォージェリ)
概要
攻撃者が利用者の認証情報を直接取得せずに、ブラウザがCookieを自動送信する仕組みなどを悪用して、認証済みの利用者に意図しないリクエストを送信させる脆弱性です。
機密性の高い処理(支払い・アカウントの削除、メールアドレス・パスワードの変更など)を勝手に実行させます。
一般的な対策
機密性の高い処理の実行前に利用者の意図したリクエストであることを確認します。具体的には以下のような対策があります。
- トークン(秘密情報)の埋め込み
- パスワード再入力
- Refererのチェック
- CookieのSameSite属性
トークンの埋め込み
状態変更を伴うリクエストを送信するフォームなどに、サーバーが生成したトークンを埋め込みます。リクエスト送信時にトークンも送信し、サーバー側で検証することで、不正なリクエストを防ぎます。
パスワード再入力
重要な処理の前に改めてパスワードの入力を求めることで正規の利用者であることを確認します。
利便性を考慮して、特に重要な処理で利用します。
Refererのチェック
重要な処理のリクエストを受け取った際にRefererを確認し、リクエストの送信元が正規のサイトであるかを検証します。
Refererとは
HTTPリクエストにはRefererヘッダが含まれる場合があり、リンク元のURLが表示されます。どこのURLから遷移したのか確認できます。
CookieのSameSite属性
Cookieには有効期限や送信条件などを制御する属性があります。代表的な属性には以下のものがあります。
| 属性 | 意味 |
|---|---|
| Expires | Cookieの有効期限を設定する。ExpiresとMax-Ageの両方を指定しない場合はセッションCookieとなり、通常はブラウザのセッション終了時に削除される |
| Secure | HTTPSの場合のみCookieを送信する |
| HttpOnly | JavaScriptからアクセスできなくなる |
| SameSite | Strict: 同一サイトからのリクエストにのみCookieを送信する Lax: 同一サイトからのリクエストに加え、他サイトからのトップレベルナビゲーションでGETなどの安全なHTTPメソッドを使用する場合にもCookieを送信する None: 他サイトからのリクエストでもCookieを送信する(Secure属性が必要) |
SameSite=Strictでは、他サイトからのリクエストにCookieが送信されません。これにより、他サイトのリンクからアクセスした場合、ログイン済みであってもサーバー側で未認証と判断される可能性があります。これは厳重で安全なものの、不便に感じる場合があります。
SameSite=Laxを指定すると、他サイトからのPOSTなどのリクエストには原則としてCookieが送信されなくなります。一方、他サイトのリンクからGETでページ遷移する場合にはCookieが送信されるため、利便性を確保しながらCSRFのリスクを軽減できます。
トップレベルナビゲーション
トップレベルナビゲーションとは、ブラウザで表示しているメインのページそのものが別のページへ移動することです。
画面が変化しない裏側で発生するリクエストでは、ユーザーが意図していないリクエストがCookie付きで送信され、ログイン状態を悪用される可能性があります。そこで、SameSite=LaxではトップレベルナビゲーションかつGETなどの安全なHTTPメソッドの場合にCookieを送信する設計となっています。
現在の主要なブラウザでは、SameSite属性を指定しなかった場合、基本的にLaxとして扱われます。
Railsでの対策
デフォルトでCSRF対策が組み込まれています。
通常のフォーム送信では、RailsによってCSRFトークンがhiddenフィールドに自動的に組み込まれます。そして送信されたトークンを検証することで、不正なリクエストを防ぎます。
Turboを使ったフォームでは、HTTPリクエストヘッダにCSRFトークンを含めて送信します。Railsのcsrf_meta_tagsヘルパーメソッドによってHTMLに出力されたトークンをTurboが取得し、リクエストヘッダに設定します。
また、RailsでもSameSite=Laxがデフォルトで設定されています。
開発者が注意すること
RESTに準じて、状態の変更を伴うリクエストではGETを使用しないようにします。
リソースの作成にGETリクエストを使用すると、他サイトからのトップレベルナビゲーションでSameSite=LaxのCookieが送信されてしまいます。また、RailsのCSRFトークン検証の対象外となるため、CSRF攻撃を受ける可能性があります。
XSS(クロスサイトスクリプティング)
概要
Webアプリケーションに悪意のあるスクリプト(JavaScript)が混入し、利用者のブラウザ上で実行される脆弱性です。Cookieが漏洩してしまい、なりすましの被害にあう可能性があります。
JavaScriptが埋め込まれる方法は様々であり、例えばURLのクエリパラメータに埋め込む方法や投稿フォームの中に埋め込む方法などがあります。
そのままの英語だとCross Site Scripting(CSS)となります。Cascading Style Sheetsと略称が被ってしまうのでXSSと略されているようです。
一般的な対策
XSSは、ユーザーの入力値などがHTMLやJavaScriptのコードとして解釈されることで発生します。そのため、出力先に応じて適切なエスケープ処理を行い、入力値がコードとして実行されないようにします。
例えば、HTML内で特殊な意味を持つ記号(&、<、>、"、')を適切にエスケープ処理します。
| 文字 | エスケープ後 |
|---|---|
| & | & |
| < | < |
| > | > |
| " | " |
| ' | ' など |
Railsでの対策
RailsにはXSSを防ぐための機能が標準で組み込まれています。
例えば、Railsは以下のようにHTMLの自動エスケープを行います。
<p><%= @user.name %></p>
<script>alert('XSS')</script>
<p><script>alert('XSS')</script></p>
通常のERBで文字列を出力する場合、開発者が毎回エスケープ処理を記述する必要はありません。
開発者が注意すること
RailsではHTMLの自動エスケープが行われますが、rawやhtml_safeを使用すると、エスケープを回避できます。
<%= raw(@user.name) %>
XSSの危険があるため、ユーザー入力値などに対して安易に使用しないようにします。
SQLインジェクション
概要
ユーザーの入力値がSQL文の一部として解釈されることで、開発者が意図しないSQL処理が実行される脆弱性です。以下のような被害が発生する可能性があります。
- DB内の情報の漏洩、改ざん
- DBサーバー上のファイルの漏洩、改ざん
- 認証の回避によりパスワードを知らないユーザーにログインされる
一般的な対策
SQL文の構造と入力値を分離します。ユーザーからの入力値はパラメータとして受け取り、入力値がSQLの命令として解釈されないようにします。
Railsでの対策
Ruby on Railsには、特殊なSQL文字をフィルタするしくみが組み込まれており、「'」「"」「NULL」「改行」をエスケープします。Model.find(id)やModel.find_by_*(引数)といったクエリでは自動的にこの対策が適用されます。ただし、SQLフラグメント、特に条件フラグメント(where("..."))、connection.execute()またはModel.find_by_sql()メソッドについては手動でエスケープする必要があります。
(Railsガイド)より引用
ハッシュ形式やプレースホルダによって安全に入力値を処理できます。
# ハッシュで条件を指定
User.where(email: params[:email])
# プレースホルダで条件を指定
User.where("email = ?", params[:email])
開発者が注意すること
ユーザー入力値をSQL文字列に直接埋め込むのは危険です。
User.where("email = '#{params[:email]}'")
セッションハイジャック
概要
セッション情報を不正に取得・利用して、正規の利用者になりすます攻撃です。
セッション情報を推測する攻撃、盗み出す攻撃、攻撃者が用意したセッションで利用者をログインさせる攻撃(セッション固定攻撃)があります。
一般的な対策
以下はServer-side Sessionで認証を行っている例をもとに考えます。
セッションIDの推測
ユーザーIDや日時といった、外部から推測可能なデータをもとにセッションIDを生成することが脆弱性の原因となります。暗号学的に安全な乱数を使用してセッションIDを生成することが対策になります。
セッションIDの盗み出し
ネットワークでの盗聴、Cookieの属性の不備による漏洩、アプリケーションの脆弱性など様々な手法があります。
以下のように対策します。
- HTTPS通信を行う
- CookieにSecure属性を設定してHTTPSの場合のみ送信されるようにする
- CookieにHttpOnly属性を設定してJavaScriptの悪用による盗み出しを防ぐ
- XSS対策
セッション固定攻撃
攻撃者があらかじめ用意したセッションIDを利用者に使用させ、ログイン後も同じセッションIDを継続して使用させる攻撃です。
攻撃者はそのセッションIDを使用することで、ログイン後の利用者になりすましてアクセスできる可能性があります。
対策として、ログイン成功時にセッションIDを再生成し、ログイン前のセッションIDを無効化します。
Railsでの対策
RailsのデフォルトであるCookieStoreは、サーバー側にセッションデータを保存する方式とは異なり、暗号化されたセッションデータ自体をCookieに保存します。改ざんを検知する仕組みも備えているため、利用者がCookieの内容を自由に変更することを防ぎます。しかし、暗号化されたCookieであっても第三者に盗まれた場合はそのまま不正利用される可能性があるため、Cookie自体の保護も重要です。
また、RailsにはHTTPS通信を強制するための設定が用意されています。
config.force_ssl = true
開発者が注意すること
Railsにはセッションをリセットできるreset_sessionメソッドが用意されています。ログイン成功時にreset_sessionを実行して既存のセッションをリセットし、その後に認証済みユーザーの情報をセッションに設定することで、セッション固定攻撃のリスクを軽減できます。
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
reset_session # セッションをリセット
session[:user_id] = user.id # セッションを再設定
redirect_to root_path, notice: "ログインに成功しました"
else
flash.now[:alert] = "ログインに失敗しました"
render :new, status: :unprocessable_content
end
end
end
おわりに
今回Railsでの対策を調べながら、セキュリティ対策があらかじめ組み込まれているというフレームワークのありがたみを改めて感じました。
一方で、開発者側がうまく使いこなすことが求められるセキュリティ機能もあります。今後も引き続き学習を続け、セキュアなアプリ開発を行えるように努めたいと思います。
参考文献
- 『体系的に学ぶ 安全なWebアプリケーションの作り方 第2版』徳丸 浩、SBクリエイティブ、2018年(出版社公式ページ)
- 『実践理解!Webアプリケーション セキュリティ』Malcolm McDonald 著、Kazuhiko Yagami 訳、マイナビ出版、2026年(出版社公式ページ)