安全なウェブサイトの作り方~失敗例~

安全なウェブサイトの作り方を読んだので、理解した内容を自分なりにまとめておきます。資料

上記は3章構成になっていてそれぞれ長めの内容なので、ここでは3章の『失敗例』について、Ruby on Rails ではどうするかについてをまとめます。

SQL インジェクション

参考資料内の SQL インジェクション例を見て、Ruby on Rails ではどのように対策できるかを確認しました。

例えば、下記ような $uid, $pass をユーザ入力とし、SQL 文を動的に生成する場合、ユーザ入力をエスケープ処理しないとデータベースへの不正利用が実現されてしまう。

-- 実行する SQL 文
SELECT * FROM usr WHERE uid = '$uid' AND pass = '$pass'

-- $uid に `'goruchan--` を入力された際の SQL 文。パスワードなしで全情報が取得される。
SELECT * FROM usr WHERE uid = goruchan

Ruby on Rails の場合、DB の操作は ActiveRecord を介して行う。

image ActiveRecord とは

検索には find_by メソッド*1where メソッド*2が使える。上記の SQL 文は下記のようにかける。

# 脆弱性のある書き方
user = User.find_by("uid = '#{params[:uid]}' AND pass = '#{params[:pass]}'")

# SQL インジェクションへの対策後
## ユーザ入力をサニタイズする
search_sql = sanitize_sql_array(["uid = ? AND pass = ?", params[:uid], params[:pass]])
user = User.find_by(search_sql)

## 位置指定ハンドラで記述
user = User.find_by("uid = ? AND pass = ?", params[:uid], params[:pass])

## キーワード引数にて記述
user = User.find_by(uid: params[:uid], pass: params[:pass])

対策後の書き方はいずれも同等で、可読性等から選択する。個人的にはキーワード引数のやつが読みやすい気がする🤔

OS コマンドインジェクション

Ruby で外部コマンドを実行する方法は、いくつかある(OS コマンドインジェクション ):

# system:コマンドを実行し、実行結果の真偽値を受けとる
result = system("ls -l")
puts result #<= 成功なら true, 失敗なら false

# exec:Ruby のプロセスを外部コマンドに置き換えて実行。コマンド実行結果は受け取らない
exec("ls -l") #<= コマンド実行に成功する場合、Ruby プロセスから抜ける
puts "この行は実行されない"

# バッククォート:外部コマンドを実行し、実行結果を文字列として取得
result = `ls -l`
puts result #<= コマンド実行時の結果

Ruby で外部コマンドをエスケープするには module Shellwords が使える。

require 'shellwords'

pattern = 'file || ls'

# エスケープせずにそのまま実行
puts "grep #{pattern}"
# => grep file || ls

# コマンドライン中に影響ある文字をエスケープする
puts "grep #{Shellwords.shellescape(pattern)}"
# => grep file\|\|\ ls

上記はいずれもシェルを起動して実行している。シェルの機能を実行させないことで OS コマンドインジェクションの脆弱性を解消できる。

Open3Ruby の標準ライブラリの一部で、外部プログラムを実行するためのモジュール。次のようにして実行できる:

require 'open3'

# popen3 メソッドを使用して外部コマンドを実行
Open3.popen3('ls', '-l') do |stdin, stdout, stderr|
  while line = stdout.gets
    puts line
  end
end

# `capture3`メソッドを使用して外部コマンドを実行
command = 'ls -l'
stdout, stderr, status = Open3.capture3(command)
puts "Standard Output: #{stdout}"
puts "Standard Error: #{stderr}"
puts "Exit Status: #{status.exitstatus}"

パス名パラメータの未チェック例(ディレクトリトラバーサル

パス名パラメータに関する脆弱性は『ディレクトリトラバーサル攻撃』につながる。下記は脆弱性を含むコードで、/etc/passwd で重要情報が見れるようになってしまう。絶対パスに対して対策しても ../../../etc/passwd として相対パスで指定されると表示してしまう。

file_name = params['file_name']
file_name = 'nofile.png' unless File.exist?(file_name)

File.open(file_name, 'rb') do |file|
  IO.copy_stream(file, $stdout)
end

ここへの対策は、パス名からファイル名だけを取り出して使用することが根本的解決になる。Ruby では singleton method File.basename が用意されているのでそれを利用すると次のコードになる:

- file_name = params['file_name']
+ file_name = File.basename(params['file_name'])

不適切なセッション管理例(セッション ID の推測)

あらゆるセッションは cookie を利用してセッション固有の ID を保存し、多くのセッションストアでは、サーバ上のセッションデータ(データベーステーブル等)を検索する時に、このセッション ID を使用する。

不適切なセッション管理の具体例として、推測可能なセッション ID を用いてしまうことがある。セッション ID 生成を自前でやる場合には、下記のような推測しやすい ID を生成せずに、乱数発生器等によって生成される安全な値を利用すること。

  • 三者が推測可能なセッション ID 生成
    • 単純な ID のインクリメント
    • Unix 時間とプロセス ID の組み合わせ
  • 三者が観測可能セッション ID 生成

Rails では、アプリケーションにアクセスする毎にセッションオブジェクトを1つ提供する。ユーザがアプリケーションを既に利用中の場合は既存のセッションを読み込み、そうでない場合は新規にセッションを作成する。 セッション ID は generate_sid 内の SecureRandom で安全な乱数発生器によって生成される。

また、Rails のデフォルトのセッションストアは CookieStore で、cookie データは改ざん防止のため暗号署名が追加され、cookie 自身も暗号化されている。

セッション

上記の通りのため、Rails をデフォルトで利用するのであれば、推測可能な ID 生成については心配不要と言えそう🧐

クロスサイト・スクリプティングの例(エスケープ処理)

XSS は Web アプリケーションにスクリプトを埋め込むことが可能な脆弱性がある場合に、利用者のブラウザ上で不正なスクリプトが実行されてしまう攻撃のこと。

XSS は根絶が難しい脆弱性である。XSSにまとめた中のエスケープ処理については、次の見落としポイントがあるので注意する:

  • ユーザ入力内容のエスケープ処理の未実施
  • テキスト形式の入力値にのみに注目してしまう
  • 404 Not Found』ページに表示する URL のエスケープ忘れ
  • 入力フォームの入力を制限する
  • ブラックリスト方式のみによる入力チェック

Ruby on Railsエスケープ処理に関して脆弱性のあるコードを考える:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end
end
<!-- app/views/posts/show.html.erb -->
<h1><%= @post.title %></h1>
<p><%= @post.content %></p>

上記はユーザ入力をそのまま表示してしまっているので、ブラウザ上で悪意あるスクリプトを実行される可能性がある。show.html.erb に対して、以下ようなエスケープ処理が必要。

- <p><%= @post.content %></p>
+ <p><%= sanitize(@post.content) %></p>

個人的には出力全てに対してエスケープ処理ができているかの確認は人手でやるのはなかなか骨が折れると感じてます。そこで、Security Scan Tools と呼ばれる XSS 検出や診断を行うツールを活用するといいそうです。

  • ZAP

    オープンソースの Web アプリケーションセキュリティスキャナで、以下の特徴がある。

    • アクティブスキャン*3とパッシブスキャン*4
    • 多彩なプラグインによる拡張性の高さ
    • API 対応による他ツールとの連携
  • Brakeman

    Ruby on Railsアプリケーションのセキュリティ解析を行うオープンソースのツールで、以下の特徴がある:

    • 静的解析
    • 標準的なセキュリティ問題の検出(SQL インジェクション、XSS 等)
    • 報告とアドバイスのレポート生成

CSRFの例

CSRF は悪意ある Web ページが、他の Web サーバへのリクエストを偽造(フォージェリ)し、ユーザの意図しない処理を他の Web サーバで実行させる攻撃のこと。被害者が認証済みにであることを利用し、悪意あるリクエストを送信することで、Web サーバにてアクションを実行させる。

具体的なイメージとしては次の図が参考になりました。

image CSRFとは?

上記のようにログイン状態にて③が実行されると、Web アプリケーション A をそのまま実行してしまうと攻撃が成立する。これは、攻撃される Web サーバにて受け取ったリクエストがユーザ本人かどうかを確認していないことが原因で発生する。

対策としては、一般的には 『CSRF トークンの使用』がある。CSRF トークンは Web サーバが生成し、cookie や フォームんどを通じてクライアントに提供される。
このトークンは悪意ある Web ページからのリクエストからは取得できないため、リクエストにこのトークンがあることで正当性を判断できる。

Ruby on Rails では、必須セキュリティトークンが用意されている。config.action_controller.default_protect_from_forgerytrue に設定すると自動的に行われるようになる。

また、Rails で新規作成したアプリケーションには次のコードがデフォルトで含まれる。

protect_from_forgery with: :exception

この設定は、Rails で生成される全てのフォームと Ajax リクエストにセキュリティトークンが自動的に含まれるようになり、セキュリティトークンがマッチしない場合例外がスローされる。

上記の通りなため、Rails を変な使い方をしない限りは CSRF に対する対策は織り込まれた状態と言えそう。

HTTP ヘッダ・インジェクションの例

HTTP ヘッダインジェクションは、攻撃者が Web アプリケーションの HTTP ヘッダに不正情報を挿入することを試みる攻撃手法で、次の攻撃につながる可能性がある。

  • セキュリティヘッダの改ざん:セキュリティヘッダ操作による XSS やクリックジャッキング等の攻撃の有効化
  • セッションハイジャック:セッションヘッダ操作によるセッションを乗っ取り
  • リダイレクト攻撃:Location ヘッダ操作による悪意あるサイトへのリダイレクト

上記の原因は、ユーザからの入力データをそのまま HTTP ヘッダで使ってしまうことにあるため、ユーザからの入力データを適切に検証、エスケープする対策が有効。具体的には、ヘッダーインジェクションの攻撃経路は、ヘッダーへのCRLF文字インジェクションに基づいているため、CRLF 文字のエスケープ対策が必要。

Rails では Rails 2.1.1 以降では redirect_to メソッドの Location フィールドから、それらの文字がエスケープされるようになったとのこと。

ヘッダーインジェクション

何かしら、ユーザ入力を用いて通常以外のヘッダーフィールドを作成する場合には、CRLF 文字のエスケープを自身で必ず実施する必要がある。

メールヘッダ・インジェクションの例

メールヘッダ・インジェクションは、主にメール送信時に攻撃者が不正なメールヘッダを挿入することで発生する。根本的な解決策としては、ユーザ入力値をメールヘッダに出力せず、メール本文に出力すれば良い。

Ruby on Rails でメール送信を行うシーンを考える。まずは脆弱性のある書き方を考える。

class UserMailer < ApplicationMailer
  def welcome_email(user, subject)
    @user = user
    mail(to: @user.email, subject: subject)
  end
end

上記は、mail メソッドを用いてメールを送信し、to ヘッダはハードコートされているが、subject ヘッダはユーザ入力をそのまま使用している。

攻撃者が改行文字を挿入することで、追加のメールヘッダを挿入できてしまう:

subject = "Welcome to My App\r\nBcc: attacker@example.com"
UserMailer.welcome_email(user, subject).deliver_now

# 生成されるメールヘッダ
# To: user@example.com
# Subject: Welcome to My App
# Bcc: attacker@example.com

上記のようなことを避けるために、ヘッダ関連のものはユーザ入力に対応させない(ハードコートする。今回の例なら 'Welcome to My App'として subject に埋め込む)、もしくは改行コードを適切に処理する必要がある。

参考

*1:一致する最初のレコード、つまりモデルのインスタンスを返す

*2:一致する全てのレコード、つまりモデルのコレクション(配列)を返す

*3:アプリケーションに対する攻撃による脆弱性検知

*4:通信傍受によるセキュリティ問題の特定