Railsプロジェクトにおけるセキュリティ（ペネトレーションテスト指摘共有）

# 0. はじめに
これまで複数の企業にてRailsプロジェクトに携わらせていただきましたが、その中で何度かペネトレーションテストを行っていただく機会に恵まれました。そこで、本記事では過去に指摘されたことのある事項を紹介したいと思います。なお、ここで紹介した指摘は実際に参画したプロジェクトにおいては、すべて修正済みとなっています。
また、Railsのセキュリティ全般については、[Railsガイド](https://railsguides.jp/security.html)をご覧ください。

# 1. Cookieセッションでログイン
### 指摘事項
Rails.application.config.session_storeで:cookie_storeが指定されており、Cookieが盗み取られると不正ログインができてしまう

### 対策
Railsはデフォルトではセッション情報はCookieのみに保存し、サーバーサイドで保存しません。これでは、Cookieが盗み取られてしまうと不正にログインが可能となってしまいます。そこで、redisやactiverecord-session_store[^1]を利用して、セッション情報をサーバーサイド側に持たせる必要があります。
リレーショナルデータベースを利用しているのであれば、activerecord-session_storeを利用するのが簡便な方法でしょう。しかし、アクセス数が多いサイトなどでは、データベースへの接続数が多くなって許容範囲を超えたり、データベースの負荷が大きくなったりするので、適した方法とは言えません。その場合は、Redisなどを利用すると良いでしょう。とはいえ、Webアプリケーションの構成要素が増えると複雑性が増し、メンテナンスの手間が増えたり障害ポイントが増えると言ったデメリットもあります。個人的には、管理画面などのアクセスが制限されているアプリケーションであればリレーショナルデータベースによるセッション管理をしてもいいのではないかと思っています。ただ、その場合でも過去のセッション情報など不要なレコードが残り続けてしまうので定期的に削除すると言ったことが必要だと思います。

[^1]: https://github.com/rails/activerecord-session_store

# 2. HSTSの指定漏れ
### 指摘事項
HTTPヘッダー内にStrict-Transport-Securityが出力されていない

### 対策
HSTS（HTTP Strict Transport Security）とは、WebサーバーがWebブラウザに対して、現在接続しているドメイン（サブドメインを含む場合もある）に対するアクセスにおいて、次回以降HTTPの代わりにHTTPSを使うように伝達するセキュリティ機構で、RFC 6797 で規定されているものです。[^2]
[^2]: https://ja.wikipedia.org/wiki/HTTP_Strict_Transport_Security
Response Headersで確認することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121304/b747cba0-399f-6b05-ff20-c398fd161294.png" width=450>


今のご時世、個人情報を扱うサービスにおいては、HTTPSでアクセスするのが基本ですから設定しておくべきものです。Railsでは、`config/environments/production.rb`ファイル内の、`force_ssl`という項目を設定することで実現できます（Rails5.0以降）。

```ruby
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
```
ただし、`force_ssl`はHTTPでアクセスがあればHTTPSへリダイレクトする機能も有します（もともとはそのための設定項目のはず）。そのため、一律force_sslとしてしまうとロードバランサーからのヘルスチェックもHTTPSになってしまいますので、ヘルスチェック用のパスを `/healthcheck` などで用意し、そのパスへのアクセスに関しては除外する様に設定することができます。

```ruby
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
config.ssl_options = {
  redirect: {
    exclude: -> request { request.path =~ /healthcheck/ }
  }
}
```

なお、HTTPからHTTPSへのリダイレクトでは、Railsだけではなく、nginxなどのWebサーバーやロードバランサーといったレイヤーでも設定しておくべきでしょう。

# 3. Cookieのsecure属性
### 指摘事項
Cookieにsecure属性が付与されていない

### 対策
Cookieのsecure属性については、IPAの情報処理技術者試験でも頻繁に出題される項目です。Cookieのsecure属性とは、MDNでは次の様な記載があります。[^3]
> Secure 属性がついたクッキーは HTTPS プロトコル上の暗号化されたリクエストでのみサーバーに送信され、安全でない HTTP では決して送信されないため、中間者攻撃者が簡単にアクセスすることはできません。

[^3]: https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies
万が一HTTPでアクセスした場合にはCookieが送信されないようにする属性というわけです。Railsでは、`config/environments/production.rb`や、`config/initializers/session_store.rb`に次の様に記述するとCookieにsecure属性が付与されます。

```ruby
# config/environments/production.rbの場合
config.session_store(secure: Rails.env.production?)

# config/initializers/session_store.rbの場合
Rails.application.config.session_store(secure: Rails.env.production?)
```

# 4. レスポンスヘッダにnginxのバージョン
### 指摘事項
レスポンスヘッダにnginxのバージョンが表示されている

### 対策
もし使用中のnginxのバージョンに脆弱性が判明した場合、攻撃者への情報提供になりえてしまいます。nginxのconfファイルを修正し、バージョンを表示させない様にします。具体的には、httpディレクティブに`server_tokens off;`を設定します。

```nginx
http {
    ・・・
    server_tokens off;
    ・・・
}
```

# 5. paper_trailで秘匿値の保持
### 指摘事項
paper_trailで更新履歴を保持していたが、crypted_passwordカラムに関しても保持している

### 対策
パスワードが更新された場合、パスワード情報を保存するカラム（`crypted_password`など）をの差分は保持するべきではありません。これらの値は暗号化されていますが、平文を計算するまでの時間稼ぎと捉え、パスワードと同様の取り扱いを行うべきです。paper_trailは差分を保存しないカラムを設定できるので、除外項目として設定します。

```ruby
class User < ApplicationRecord
  has_paper_trail ignore: %i[id created_at updated_at crypted_password]
end
```

# 6. ログアウト処理の不備
### 指摘事項
ログアウト時にはクライアントサイドのCookie削除だけでなく、サーバーサイドでも削除する

### 対策
ログアウト処理において、Cookieを削除すればサーバーサイドのセッション情報を復元できずログアウト状態となりますが、サーバーサイドのセッション情報が残ったままだと過去のCookieを用いてログイン状態とすることが可能となってしまいます。そのため、ログアウト処理時にはcookieだけでなく、sessionをclearするようにします。

# 7. パスワードリセット機能で未登録のメールアドレスが入力された場合の処理不備
サービスの概要を説明します。ログイン・ログアウトがあるサービスで、メールアドレスとパスワードを入力してログインしてもらうが、パスワードを忘れた際のためにパスワードリセット機能が存在するというサービスになります。

### 指摘事項
登録されていないメールアドレスを入力された際、「メールアドレスを正しく入力してください」というエラーメッセージが出ている。（存在するメールアドレスが入力された場合は、「メールを送信しました」のメッセージが出る。）

### 対策
ブルートフォース（総当たり）アタックによって、登録されているメールが確認されてしまいます。登録されていないメールアドレスの場合でも、登録されている場合と同じメッセージを出す様にします。

# 8. idでの連携
### 指摘事項
Orderモデルのレコードを作る申し込みフォームにおいて、別のレコード（仮にUserとする）と紐づけるために、inputタグのhiddenでUserのid（user_id）を渡しており、user_idが存在する場合と存在しない場合でレスポンスステータスが異なっており、存在するuser_idを確認することができる。

### 対策
Railsではしばしばインクリメントされたidを利用することがあります。しかし、idは連番なので攻撃者にとって容易に想像しやすくなってしまいます。攻撃者はidをインクリメントしながらリクエストすることで存在するuser_idを確認することができるので、uuidなど連番でない容易に想像されないもので連携するようにします。ペネトレーションテストを依頼した会社によれば、32文字や64文字で連番じゃない値にすれば攻撃者としても厳しいとのことです。しかし、それでも総当たりされれば発見できてしまうので、idを暴露しない様な実装をすることが必要です。

# おわりに
脆弱性はアプリケーションエンジニアが気をつけていても、どうしても埋め込んでしまうものです。本記事では、Cookieの設定やnginxなどの指摘事項もありましたが、仕様で気をつければ生み出すことがなかったものもあります。セキュリティの強化とユーザービリティーの向上はときに相反するものもありますが、しっかりとその意義を理解して説明する必要があります。
また、今回挙げたもの意外にも脆弱性となりうるものはたくさんあります。一度ペネトレーションテストを実施してもらうのが良いでしょう。