ランダムURLのセキュリティ

現在のサービスの要件が、ログインの煩わしさを省いてユーザビリティを高める目的で、ログイン機能なし、ランダムURLによってアクセスを絞り込むのみ、というものになっている。

これについて、現行の実装と、考えられるセキュリティリスクをリストアップし、これに対する機能追加の方法についても吟味する。そのコストや効果を検討し、実装方針を固めることにした。

現行の仕様

現行では、以下のようなランダムなIDを自動生成してDBに文字列として保存している。

self.id = SecureRandom.alphanumeric(20)

それをそのままルーティングにすることで以下のようなURLを実現している。

http://localhost:3000/rooms/UiC8Z6pYgQnz8RMhqWg8

基本的にはこのURLさえ知ることができれば、作成者以外でも作成者の情報を閲覧することができる。

このサイトのデータベースに登録される秘匿性の高い情報は以下の通り。

  • Eメール(他の情報との組み合わせ次第で個人情報となる)
  • 部屋の間取り図(十分なデータを持っている場合、住所を特定できるかも??)
  • 部屋のキズの写真(個人情報が写真に紛れる可能性?)

それほど致命的な個人情報はないが、漏洩すれば怒られそうなものもある。

現行では、漏洩のリスクを明記して、他者へURLを渡さないよう注意喚起をすることで、これらの漏洩リスクを許容してもらうサービスとなっている。また、データベースの保存期間を短くする(10日)ことで、長期間リスクにさらされる状況を避けている。

考えられるセキュリティリスク

リスク1: ユーザーがURLを公開してしまう。

リスク2: リクエスト情報からURLが露見してしまう。

例えば以下の場所を閲覧されてしまえばURLが参照できるそうだ。

Webサーバのアクセスログ
ファイアウォールログ
プロキシサーバのキャッシュやログ
ブラウザのキャッシュや履歴

参考: 1-4. クエリストリングから情報が漏れる

リスク3: DBから露見してしまう。

現行ではURLで使用されているランダム文字列がそのままIDとして保存されているため。

参考: https://railstutorial.jp/chapters/advanced_login?version=5.0#sec-remember_token

リスク4: 通信を傍受される。

HTTPSによるSSLでの対策はしている。

参考: https://railstutorial.jp/chapters/advanced_login?version=5.0#sec-remember_token

対応策としての機能追加

上記のリスクに対応するためには以下のような機能追加が考えられる

対応策1: DBのハッシュ化

DBに不正アクセスされた場合にURLを盗まれてしまうと自由にアクセスできるようになってしまうので、これをハッシュ化する。

参考: 第9章 発展的なログイン機構 - Railsチュートリアル

参考: ハッシュ化・ハッシュ値ってなんだ?から、パスワードの仕組みまで。 – 自主的20%るぅる

IDをハッシュ化すると再利用不能になるので、IDと別にURLというカラムを作り、それをハッシュで保存する。

ルーティングでURLにID以外を使うことはできそう。

参考: Railsのルーティングでid以外の数値カラムをURLに使うときの注意点 - Qiita

ID+URLのようなPARAMを渡して、コントローラーでモデルのauthenticated?を呼び出す。

# controller
room = Room.find_by(id: params[:id])
room.authenticated?(params[:url])

モデルでは、ハッシュ化されて保存されているurl_digestとリクエストで渡されたurlをハッシュ化したものを比較して同一かどうか調べるようにする。

# model
def authenticated?(url_token)
    BCrypt::Password.new(url_digest).is_password?(url_token)
end

この場合、現行のままDBに侵入された時のリスクのみ回避できる。

対応策2: URLとクッキーのトークンの二つが揃わないと所定のページが閲覧できないようにする。

同じブラウザのみでの閲覧を許し、URLが知られても他人にはアクセスできなくする。複数ブラウザをまたいで閲覧できなくなり、ブラウザ間で同期しているブックマークが機能しなくなるという悪い副作用がある。

個別ページのURLを作成したタイミングで、クッキーにトークンを保存し、トークンをハッシュ化したダイジェストをDBに保存する。毎回アクセス時にURLの保持するダイジェストとブラウザのクッキーが保持するトークンが一致していることを確認するようにする。

参照: 第9章 発展的なログイン機構 - Railsチュートリアル

対応策3: メールで保存したURLにトークンを仕込んでメールからは所定のページが閲覧できるようにする

本来はログイン機能の登録時にメールでの認証を行う機能を流用し、トークン付きのリクエストを仕込んだリンクをメールによってユーザーの手元に保管させる。URLを知っていてもトークンがない場合はアクセスできなくする。複数ブラウザをまたぐことも可能になる。

メールを発行したタイミングでDBにトークンをハッシュ化したダイジェストを保存する。トークンを仕込んだリンクがメール本文に記載されている。アクセス時にはURLの保持するダイジェストとトークンが一致していることを確認するようにする。

参照: 第11章 アカウントの有効化 - Railsチュートリアル

対応策4: ログイン機能を追加

いっそのことログイン機能を実装してしまう。ユーザーのないサービスなので、各ページにログインする機構となる。

この場合、一般的なログインの実装が使えるので、設計コストがないが、対応策の中では一番大掛かりな機能追加となる。また、後述するように、本来の仕様や設計思想には反する実装となる。

セキュリティ効果とユーザビリティとコストの検証

セキュリティを高めようとすればするほど、ログインの機能をそなえたサービスに近くなり、本来の要件である、ログインの煩わしさを省いてユーザビリティを高める目的と矛盾するようになる。ランダムURLの採用は、ログインの代替を期待していたので、その本来の役割も失われる。

ただし、現行でもEメールの登録はほとんど必須なので、パスワードのないログイン機能の追加は、現行と比べてそこまでユーザビリティを下げないかもしれない。むしろ、機能追加のコストをかけるべきかという観点の方が重要になってくる。

もしコストを許容するならば、ランダムURLは閲覧のみ、編集・管理権限はログインによって与えられる、という役割分担はありえる。あるいは、Eメール登録すれば文書保存でき、Eメールなしならその場で破棄するという場合分けによって、ユーザーの選択肢を増やすという機能追加はあり得る。

結論

リスクを許容してもらうように注意喚起をする現行の状態が一番コストが低い。追加のセキュリティ対策をリリース後に回していくのが、自作サービス課題の要件とも一致するように感じた。