Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

保存版: Railsアプリケーションのセキュリティベストプラクティス(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

参考: 週刊Railsウォッチ20221011 Railsのセキュリティベストプラクティス

日本語タイトルは内容に即したものにしました。原文の章インデントは訳文で一部を変更しています。

以下のRailsセキュリティガイドも合わせてお読みください。

参考: Rails セキュリティガイド - Railsガイド

保存版: Railsアプリケーションのセキュリティベストプラクティス(翻訳)

Webアプリケーションを構築するときは、パフォーマンスや使い勝手を重視するのはもちろんですが、セキュリティにも注目する必要があります。ハッキング手法は、技術の進化と変わらない速度で常に進化を繰り返していることをお忘れなく。ユーザーとそのデータを保護する方法はぜひとも知っておかなければなりません。

本記事では、セキュアなRailsアプリケーションの作り方を紹介します。デフォルトのRailsフレームワークがセキュアであることは知られていますが、デフォルト設定だけでは夜もおちおち寝られないほど不十分です。

セキュアなコードを書くのに役立つコーディングのベストプラクティスや開発で役立つ習慣をいくつかご紹介します。

それでは始めましょう。

アプリのセキュリティ: コーディングのベストプラクティス

モダンなWebアプリケーションの多くは複雑です。データソースを複数利用することもあれば、さまざまな認証方法の他にカスタム認証ルールを扱うこともあります。SQLインジェクションの回避方法や、ユーザーにデータの読み取りだけを許可することを知っているだけでは不十分です。

Railsアプリを構築するときは、コンフィグを正しく行い、アプリケーションを安全に設計し、攻撃を防げるコードを書かなければなりません。

本記事では以下のトピックを扱います。

アプリケーション設定
デフォルトのコンフィグは模範的に作られていますが、少し手を加えることでさらに強化できます。
ビジネスロジック
アプリケーションのコードレベルだけではなく、設計レベルでもセキュアにするべきです。しかし、この鉄則はMVP(Minimum Viable Product)を急いで提供しなければならなくなると無視されがちです。
コントローラのコード
これらのクラスはアプリケーションのエントリポイントになるので、高信頼性アプリケーションを設計するうえで常に特別な注意を払っておく必要があります。
モデルのコード
問題の多くはデータベース周りに関連するので、データの一次ソースとの通信をセキュアに設計・実行することが不可欠です。
ビューのコード
ビューはデータをブラウザに公開する場所なので、ここもハッカーに狙われやすくなります。ユーザーのデータやプライバシーを危険にさらすものは一切画面に表示しないようにしておかなければなりません。

🔗 アプリケーション設定

すべては設定ファイルから始まります。ほとんどの設定変更はアプリケーションを再起動しないと反映されません。Railsフレームワークの作者たちは安全なデフォルト設定を作るために努力を重ねていますが、少し手を加えることでさらに強化できます。

🔗 SSLの強制

Railsアプリケーションが常にHTTPSプロトコルで通信するよう強制できます。これを行うには、config/environments/production.rbファイルを開いて以下の行を設定します。

# config/environments/production.rb
config.force_ssl = true

この設定によってアプリケーションで以下が行われます。

  • アプリケーションがHTTP通信のリクエストを受け取るたびに、リクエストをHTTPSプロトコルにリダイレクトする。
  • cookieにsecureフラグを設定する。これによってcookieがHTTPリクエストで送信されなくなる。
  • アプリケーションがTLSだけを用いることをブラウザに記憶させる(TLS: Transport Layer SecurityはHTTPSで用いられる拡張プロトコルです1)。

🔗 CORS

CORS(Cross-Origin Resource Sharing)は、アプリケーションAPIが通信してよいWebサイトを定義するセキュリティ機構です。もちろん、モノリスアプリを構築する場合はCORSによる保護を気にする必要はありません。

RailsでAPIアプリケーションを構築する場合は、rack-cors gemを追加インストールすることでCORSを設定できます。

cyu/rack-cors - GitHub

rack-corsをインストールしたら、config/initializers/ディレクトリの下にcors.rbファイルを作成し、(リクエストメソッドも含めて)Webサイトがアクセスしてもよいエンドポイントを以下のように定義します。

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
 allow do
   origins 'https://your-frontend.com'
   resource '/users',
     :headers => :any,
     :methods => [:post]
   resource '/articles',
     headers: :any,
     methods: [:get]
 end
end

上の設定例では、your-frontend.comというWebサイトに対して、/usersエンドポイントへの呼び出し(POSTメソッドのみ)と、/articlesエンドポイントへの呼び出し(GETメソッドのみ)を許可しています。

追記(2022/11/18)

以下のご指摘ありがとうございます🙇。上記訳文を修正しました。

▶修正前の訳文(クリックすると開きます)

上の設定例では、your-frontend.comというWebサイトの/usersエンドポイントにはPOSTメソッドの呼び出しのみを許可し、/articlesエンドポイントにはGETメソッドの呼び出しのみを許可しています。

🔗 環境変数をセキュアに扱う

APIキーやパスワードなどの秘密情報(credential)は、絶対にソースコードに直接書き込んではいけません。秘密情報が何かのはずみで外部に流出する可能性や、権限を持たない人物にアプリケーションの秘密リソースへのアクセスを許してしまう可能性が生じます。

credentialを安全に保存する方法はRailsフレームワーク自身が提供しています。ただしRailsバージョンによって実装が異なります。

Rails 4
このときは'secrets'という機能名でした。秘密情報は、Gitリポジトリに登録されないconfig/secrets.ymlファイルに保存します。
Rails 5
このときは'credentials'という機能名でした。秘密情報はconfig/credentials.yml.encファイルに暗号化した形で保存されます。このファイルはconfig/master.keyファイルのキーを用いて編集できます。YAMLファイルは暗号化されているのでGitリポジトリに登録できますが、master.keyファイルはリポジトリに登録しません。
Rails 6
このときも'credentials'という機能名でした。環境ごとに異なるcredentialを保存できます。このため、credentialsを使う場合はどの環境でもYAMLファイルの暗号化とmaster.keyによる復号が必須です。

この他に、これらの値を常にサーバーレベルで設定して、サーバー環境でのみ値が読み込まれるようにする方法もあります。ローカル環境では開発者が個別に値を設定します。

🔗 ビジネスロジック

セキュリティは、コードを書くときだけではなく、アプリケーションの設計段階から考えておくべきです。

ビジネスロジックは実世界で適用されるルールの集まりであり、ビジネスロジックをコードに落とし込むことが開発の目標です。残念ながら、ビジネスロジックをコードに落とし込むときに弱点が発生して、アプリケーションでセキュリティ問題を引き起こすことがあります。

🔗 認証と認可のルールを強化する

最初に認証(authentication)と認可(authorization)の違いについて説明しておきます。これらの違いはたまに誤解されていることがあります。

認証
ユーザーのログイン名とパスワードをアプリケーションのデータベースで検証すること。
認可
サインインしたユーザーのロール(role)を検証し、それに基づいてユーザーごとに表示する情報を変える。たとえば、adminロールを持つユーザーはアプリケーションのユーザーリストにアクセスできるが、一般ユーザーはほとんどの場合アクセスを許されない。

認証のセキュリティレベルを強化するには、エンドユーザー向けに高度な標準を設定するのが有効です。

強力なパスワード
単純なパスワードや弱いパスワードの利用を禁止する厳密なパスワードポリシーを設定する。当然ながらユーザーが自分のパスワードを共有することは制限できないが、推測されにくいパスワードを使わせることは可能。
2要素認証
パスワードが万一漏洩した場合でもアカウントを保護する追加の防御層。
パスワードの暗号化
パスワードは絶対に平文のままデータベースに保存してはならない。パスワードが暗号化されていれば、万一データベースが流出してもユーザーのパスワードは奪えなくなる。

🔗 認可のベストプラクティス

複雑なアプリケーションでは、データを管理するためにユーザーにさまざまなロールを割り当てる必要がおそらく生じるでしょう。ビジネスロジックが大きく育つに連れて、情報漏えいにつながりかねないミスを避けながらすべてのロールを制御するのがいっそう困難になる可能性があります。

いくつかの望ましい対処方法が知られているので、これらに沿うことで問題を回避できます。

  • 認可ロジックは1箇所にまとめる: コードが散らばっているとビジネスロジックを適切に変更するのが難しくなる。ルールが1箇所に集約されていれば変更しやすい。
  • 認可のルールを明文化する: 明確に定義されたルールでビジネスロジックを検証できなければ、アプリケーションが安全かどうかを判断しようがない。
  • ロールは単一ユーザーにではなくグループに対して設定する: 個別のユーザーにルールを定義するよりも、権限ごとにグループを作る方が認可を制御しやすい。

言うまでもなく、質の良いコードレビューと十分なテストも、既存のビジネスロジックにバグを持ち込まないために欠かせません。

追記(2022/12/05)

この他に、外部からのアクセスを想定していないURL(Sidekiqなど)がpublicになっていないかどうかも確認しておきましょう(参考文献↓)。

🔗 コントローラのコード

コントローラはMVCアーキテクチャの最初の層であり、ユーザーからのリクエストを処理する場所です。入力情報はアプリケーションの他の層にも伝搬するので、入力情報を適切にフィルタすることが重要です。

🔗 入力パラメータをフィルタする

paramsの値は、絶対にそのままの形でアプリケーションに渡してはいけません。Railsにはstrong parameters機能があるので、渡すデータを手軽に制御できます。

たとえばUserモデルを更新したいとしましょう。

class UsersController < ApplicationController
  def update
    current_user.update(params[:user])
  end
end

上のような書き方では、誰かがparamsの値を細工するとUserモデルの他の属性(adminフラグなど)を更新できてしまいます。このような事態を避けるために、モデルに渡すパラメータは以下のようにフィルタしておきましょう。

class UsersController < ApplicationController
  def update
    current_user.update(user_params)
  end

  private

  def user_params
    params.require(:user).permit(:first_name, :last_name)
  end
end

訳注(2023/01/27)

以下の記事によると、Railsアプリでransack gemをデフォルト設定のまま使うと、パスワードリセット用トークンを盗まれる可能性があることが指摘されています。ransack gemを使う場合は、strong parametersに加えてモデルにransackable_attributesなどでホワイトリストを指定するといった対策も行いましょう。

参考: Ransacking your password reset tokens | Positive Security

activerecord-hackery/ransack - GitHub

🔗 スコープでデータ漏えいを防ぐ

ユーザーには、そのユーザーのものでない無関係なデータを表示したくないのが普通です。コード設計をしくじると、URLアドレスを細工してレコードの一部を表示できてしまう可能性があります。以下の例を考えてみましょう。

class MessagesController
  before_action :authenticate_user!

  def show
    @message = Message.find(params[:id])
  end
end

一見問題はなさそうです。コントローラはゲストから保護されていて、messageオブジェクトを代入しています。しかしこれでは、ユーザーがURLアドレスで自分のidを変更するだけで別のユーザーのmessageを取得できてしまいます。これは極めて危険な状況です。

これは、以下のように指定のコンテキストに限定するスコープを使うことで回避できます。上の例の場合はcurrent_userがそのスコープです。

class MessagesController
  before_action :authenticate_user!

  def show
    @message = current_user.messages.find(params[:id])
  end
end

🔗 安全でないURLリダイレクトを回避する

ユーザー入力に基づいてリダイレクトする危険性のわかりやすい例を考えてみましょう。

redirect_to params[:url]

上のような書き方は絶対にしてはいけません。ユーザーを任意のパスにリダイレクトできてしまうからです。このセキュリティ問題を防ぐ最もシンプルな方法は、ユーザー入力をリダイレクトに渡さないことです。それができない事情がある場合は、以下のようにホスト部を含まないパスに常にリダイレクトする方法もあります。

path = URI.parse(params[:url]).path.presence || "/"
redirect_to path

🔗 モデルのコード

Railsがコントローラで入力を解析したら、おそらくモデルを呼び出してデータベースから情報を受け取るでしょう。モデルはデータベースとのやり取りに加えて重要な計算処理も担当するので、ここもハッカーに狙われやすい場所です。

🔗 SQLインジェクションを回避する

SQLインジェクションは、外部からデータベースの情報にアクセスするのによく使われる攻撃手法のひとつです。Railsフレームワークはこうした脅威から私たちを守ろうとしていますが、そのような事態にならないようなコードを書く必要もあります。

一般に、ユーザー入力をクエリの一部に直接渡すことは避けるべきです。

User.joins(params[:table]).order('id DESC')

どうしてもユーザー入力を渡す必要がある場合は、必ずユーザー入力をバリデーションし、入力が不正な場合はデフォルト値を渡すこと。

valid_tables = %w[articles posts messages]
table = valid_tables.include?(params[:table]) ? params[:table] : "articles"
User.joins(table).order('id DESC')

SQLインジェクションについて、やってはいけない事例が以下のサイトで詳しく紹介されています。

参考: Rails SQL Injection Examples

🔗 コマンドインジェクションを防ぐ

プログラムが任意のコードを実行できてしまうようなメソッド(execsystem`evalなど)の利用は極力避けること。これらの関数には絶対にユーザー入力を渡さないでください。

アプリケーションでユーザーにコードの実行を許可したい場合は、別のDockerコンテナで実行すべきです。さらに、ユーザー入力を実行する前に以下のように危険なメソッドをあらかじめ削除しておくとよいでしょう。

module Kernel
 remove_method :exec
 remove_method :system
 remove_method :`
 remove_method :eval
 remove_method :open
end

Binding.send :remove_method, :eval
RubyVM::InstructionSequence.send :remove_method, :eval

🔗 安全でないデータシリアライズを回避する

安全でないデシリアライズを使うと、アプリケーションで任意のコードを実行される可能性があります。

JSONを以下のような形でシリアライズする予定がある場合は、

data = { "key" => "value" }.to_json
# => "{\"key\":\"value\"}"

loadメソッドやrestoreメソッドではなく、parseメソッドを使うこと。

# bad
JSON.load(data)
JSON.restore(data)

# good
JSON.parse(data)

YAMLを以下のような形でシリアライズする予定がある場合は、

data = { "key" => "value" }.to_yaml
# => "---\nkey: value\n"

Marshalモジュールのloadメソッドやrestoreメソッドではなく、Psychsafe_loadメソッドを使うこと。

# bad
Marshal.load(data)
Marshal.restore(data)

# good
Psych.safe_load(data)

🔗 ビューのコード

ビューでレンダリングしたものは全部エンドユーザーに表示されます。悪意のあるコードがレンダリングされると、信頼できないWebサイトやデータソースを見せられたユーザーに直接悪影響が生じます。

🔗 CSSインジェクションを回避する

CSSコードはWebサイトのスタイルを変更するだけなので、常にセキュアだと思われています。しかし、アプリケーションでユーザー定義のスタイルを許可すると、CSSコードのインジェクションというリスクが常につきまといます。

ユーザー定義CSSのよく知られた例は、以下のようなページ背景色のカスタマイズです。

<body style="background: <%= profile.background_color %>;"></body>

これでは、悪意のあるユーザーが背景色の値にURLを入力するとアプリケーションが自動的に読み込んでしまい、現在コンテンツを見ているユーザーに損害を与える可能性があります。このような事態を防ぐには、ユーザーに任意の値の入力を許すのではなく、事前定義済みの値リストから選択させるようにします。

🔗 レンダリング結果をサニタイズする

Railsの新しいバージョンでは、出力結果がデフォルトでサニタイズされます。ユーザーが入力したHTMLやJavaScriptコードがアプリケーションでレンダリングされたとしても、それらのHTMLやJavaScriptコードはエスケープされます。

ユーザーが定義したHTMLをレンダリングさせたい場合は、必ずどのタグをレンダリングしてどのタグをエスケープするかを事前に定義しておくこと。

<%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %>

上のコードでは、ユーザーコメントのレンダリングでHTML要素のstrongemaのみを許可しています。

🔗 コードコメントに秘密情報を書かないこと

この注意事項は、主にWebアプリケーションの仕組みに不慣れな若手フロントエンド開発者向けです。コードコメントに秘密情報を書き込んではいけません(特にビューに書くとエンドユーザーから丸見えになってしまいます)。

<!--- このサービスのパスワードは1234だよ -->
<%= @some_service.result_of_search %>

🔗 Railsアプリケーションをセキュアにするための習慣

Railsアプリをセキュアに保って技術的負債を減らすために、開発で有用ないくつかの習慣を身につけましょう。小さな予防措置を普段から絶えず繰り返し実施する方が、後でコードベースの重要な部分をリファクタリングするよりもずっと苦痛が少なくなります。

🔗 Railsや他のライブラリをまめにアップグレードする

ライブラリのマイナーアップグレードでもない限り、アップグレードは時間のかかる苦行になりがちです。新しい安定版がリリースされたら、そのたびにライブラリをアップグレードする習慣を養いましょう。そうすれば、アプリケーションはセキュリティとパフォーマンスの両面において良好な状態を保てます。

普段の開発でGitHubを使っているのであれば、Depfuなどの拡張でまめなアップデートを代行するのが便利です。

🔗 セキュリティ監査を実施する

外部企業に高価な監査を依頼しろという意味ではありません。多くの場合、Brakemanなどのツールをインストールし、コミットやプルリクのたびにコードをスキャンすれば十分です。

また、Gemfileをスキャンして、コミュニティが発見したセキュリティ問題のために更新の必要なgemを探すのもよい考えです。これはbundler-auditで自動化できます。

🔗 適切なログ出力ストラテジーを用いる

多くの場合、ログは普段見返すこともないような何千行もの情報にすぎません。しかしユーザーが攻撃を受けたり不審な操作が行われたりする可能性は常にあります。

検索しやすい詳細なログが残されていれば、そのような事態が発生したときに、そこから情報を収集して今後同様の問題を防ぐのに役立てられます。

まとめ: Railsアプリをセキュアにしよう

本記事では、Railsアプリでデータ漏洩のリスクを軽減するためのセキュリティベストプラクティスを紹介しました。

Railsアプリケーションの安全を確保するのは簡単な話ではありません。フレームワークのデフォルト設定を忠実に守るだけでは不十分で、セキュリティ問題を発生させない方法を熟知しておかなければなりません。ここ数年、さまざまなインジェクション攻撃やリモートコード実行攻撃などについての情報は普及しましたが、それでも自分たちのアプリが100%影響を受けないという保証にはなりません。

こまめなアップグレード、セキュリティ監査、適切なログを出力する文化の重要性も忘れてはいけません。これらが組み合わさることで、Railsアプリケーションの安全を高めるのに役立つでしょう。

Happy coding!

追記: Ruby Magicの記事が公開されたらすぐ読みたい方は、こちらのフォームでRuby Magicニュースレターをお申し込みいただければ見逃しの心配がなくなります。

関連記事

Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳)


  1. 編集部注: ここは原文と異なりますが社内レビューに基づいた訳文としました。参考: Transport Layer Security - Wikipedia 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。