ドラえも⚫︎で理解するCSRF
はじめに
※コメントにて徳丸浩先生(@ockeghem)に間違いをいくつかご指摘頂き修正中です。
また、@rudorufu1981様よりブラウザの同一オリジンポリシーについての補足を頂いております。
ぜひ記事の下部、コメント欄までご覧頂きますようお願いいたします。
ご指摘や補足、本当に有難うございます。
【追記2023.12.2】
同一オリジンポリシーの補足についても徳丸先生のご見解コメントを頂いております。ぜひそちらもご確認下さい。
【追記2023.12.5】
ご指摘を受けて同一オリジンポリシーはCSRFと直接の関連はない事から、取り消し線にて削除致しました。
対象読者
・HTTPの特性
・セッション管理
・ブラウザの同一オリジンポリシー
・CSRF(Cross-Site Request Forgeries)
⚫︎上記の言葉を聞いてイメージができない人
⚫︎Railsの<%= form_with do |f| %>
などを「HTMLのままでもいいじゃん...」と思っている人
最近RUNTEQ生もAPIやReactなどのフロントエンドの技術を使用することが増えています。
情報や状態をフロント側で持つことはセキュリティ的に慎重に設計する必要があることを、私は何も考えていませんでした。Railsがよしなにやってくれてた😇
自戒も込めて、今回はCSRFについて調べることにしました。
本記事はエンジニア未経験者が、同じエンジニア未経験者に向けて「なるべく分かりやすく、イメージを掴むことができる」ことを重視して書いております。正確でない部分もあるかと思いますが、マサカリ大歓迎ですので、ぜひコメント欄にて補足や訂正をいただけますと幸いです。
今回説明すること
- HTTPの特性
- セッション管理
- ブラウザの同一オリジンポリシー
- CSRF(Cross-Site Request Forgeries)
1. HTTPの特性
Webの特性を知らずに、セキュリティ対策を考えることはできません。そこで、まずはHTTPの特性についてみていきたいと思います。
HTTPとは「Hyper Text Transfer Protocol」の略で、WebサーバとWebクライアントがデータを送受信するために利用されるプロトコル(通信手段)です。
もう少し分かりやすく考えてみましょう。
HTTPはのび⚫︎君に似ています。
(アッ...早速まさかり案件......🪓🔥)
のび⚫︎君(HTTP)の一番の特徴は、ステートレスであることです。
ステートレスとは、ステート(状態)を保持しないことです。
のび⚫︎君はすぐに忘れます。
昨日言ったことどころか、今さっき言ったこともやったことも覚えてない!
それが、のび⚫︎君(HTTP)!
(異論は受け付けます😇)
ステートレス以外にも、のび⚫︎君(HTTP)には下記のような特徴があります。
・たくさんの情報は保持できない
・思ったことが全部顔と口に出てバレる(平文での通信)
・道具を使うと記憶力マシになるけど、注意していないと何かと問題を起こす脆弱性がある(セッションやキャッシュの使用)
・おおらかでシンプルがゆえに可能性の幅があり、愛されて普及している
(伝われ...!)
のび⚫︎君(HTTP)のお仕事は、ドラえも⚫︎(Webサーバ)にリクエストをして、目的の道具(HTMLや画像など)をもらってくることです。
「ドラえも⚫︎、タケコプターを貸してよ」
「ドラえも⚫︎、宿題が自動でできる道具はない?」
これを、リクエストメッセージと呼びます。
実際のリクエストメッセージは、下記の画像のような構成になっています。
リクエストメッセージの1行目をリクエストラインと呼び、メソッド、パス(URI)、プロトコルバージョンを空白で区切って表します。
リクエストメッセージによって要求されたHTMLファイルや画像をドラえも⚫︎(Webサーバ)は探します。
「どこにあったかなぁ(四次元ポケットゴソゴソ)」
ドラえも⚫︎(Webサーバ)は答えます。
「ほら、タケコプター(200 OK)」
「全く君はしょうがないな、そんな道具はないよ!(404)」
これを、レスポンスメッセージと呼びます。
リクエストに対しての処理結果を伝えてくれるのが1行目のステータスラインです。
ステータスコードには100の位に意味があって分類されています。詳しくは下記を参照してください。
ここまでで言いたいのは、のび⚫︎(HTTP)はドラえも⚫︎(Webサーバ)へお願いを送ったりその結果を受け取る通信手段であること。そして、のび⚫︎(HTTP)は自分が言ったこともドラえも⚫︎が言ったこともすぐ忘れるステートレスという特徴があるということです。
2. セッション管理
のび⚫︎君(HTTP)に、何度も何度も同じことを伝えるのって大変ですよね。できれば、この前やった授業の内容とか、お母さんとの約束事とか覚えておきたい。
「そうだ!自分(HTTP)が覚えられないなら、他の人が覚えたら良いじゃない!」
他力本願のび⚫︎君です。
その仕組みが、「セッション管理〜」
(大山のぶ⚫︎世代です🫡)
セッションとは、状態のことです。
セッションは、Webアプリケーションにおいて「状態」を保持する仕組みです。
例えば、ショッピングカートやログイン状態など、アプリケーションがユーザーの活動を追跡し、その状態を覚えておく必要があります。HTTPはステートレスなので、この状態管理を実現するためにセッションという機構が利用されます。
セッションを実現する仕組みの一つがクッキー(cookie)です。
ドラえも⚫️(Webサーバ)は、のび⚫️君(HTTP)に直接何かを覚えてもらうことが難しいので、お手伝いとしてクッキー🍪を使うのです。
もう少し具体的にみていきたいと思います。
①まず、のび⚫️君が何かをお願い(リクエスト)します。ドラえも⚫️はそのお願いに対して答え(レスポンス)を返します。
繰り返しになりますが、のび⚫️君(HTTP)は前回のお願いを覚えていません。そこで、セッションとクッキーが登場します。
③ドラえも⚫️(Webサーバ)は、のび⚫️君に代わってブラウザに「名前=変数」の組を覚えてもらうように指示します。これが、クッキー🍪です。
④のび⚫️君がリクエストする時、ブラウザはクッキー🍪を持っています。のび⚫️君がドラえも⚫️にお願いする時、クッキー🍪をドラえも⚫️に渡します。そして、ドラえも⚫️はクッキー🍪を見て、以前の情報を思い出し、のび⚫️君の状態を保持してくれます
具体的にRailsではどのように使われいるのでしょうか。
ユーザーのログイン情報をセッションで保持する過程をみてみましょう。
通常、ログインなどのプロセスでユーザーが認証された後、ユーザーIDがセッションに保存されます。
def log_in(user)
session[:user_id] = user.id
end
上記は、gem sorceryのlog_inメソッドの中身です。
ログイン情報を元に『sessionに[:user_name]という名前をつけて「user.id」を格納してね』と、ドラえも⚫︎(Webサーバ)が指示してます。
次に、current_user
メソッドを例に挙げてみます。
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
先ほどのドラえも⚫︎(Webサーバ)からの指示で、ユーザーIDがセッションに保存されます。
session[:user_id]
はそのIDを取り出しています。
User.find_by(id: session[:user_id])
は、取得したユーザーIDを使用してデータベースから該当するユーザーを検索しています。
検索結果は@current_user
に代入されます。また、||=
を使用して、既に@current_userが設定されていれば再度検索を行わないようにしています。
このように、本来はHTTPリクエストのやり取りの中でログインユーザーの情報は忘れ去られ、毎回ユーザー情報を渡さなければいけませんが、セッション管理を行うことでHTTPリクエストに情報を持たせることが可能です。
(だんだんドラえも⚫︎で例えるの辛くなってきたぞ...)
3. ブラウザの同一オリジンポリシー
ここまで、HTTPとセッション管理についてみてきました。次は、のび⚫️君(HTTP)には生活のルールがあるよ、というお話です。
同一オリジンポリシーとは何か?
同一オリジンポリシーは、Webセキュリティ上の原則で、異なるオリジン(ドメイン、プロトコル、ポート番号のいずれかが異なる)からのリソースへのアクセスを制限するルールです。
例えば、下記はどれが同一オリジンでしょうか?
(a)http://www.example.jp/index.html
(b)http://www.example.jp:80/request.php
(c)https://www.example.jp/index.html
(d)http://img.example.jp/flower.jpg
(e)http://www.example.jp:8080/index.html
(a)と(b)はどちらもスキームがhttp、ホストがwww.example.jp、ポートが80なので同一オリジンといえます。一方、(a)と(c)ではスキームが、(a)と(d)ではホストが、(a)と(e)ではポートが異なるため、いずれも同一オリジンではありません。
知識無さすぎてこの説明すら難しい......😇
では、ドラえも⚫︎で同一オリジンをイメージしてみましょう。
のび⚫︎君の友達のお家(同一オリジン):
のび⚫︎君としず⚫︎ちゃんは同じ学校に通っており、同じ町に住んでいます。のび太君は気軽に友達の家に行くことができます。これが同一オリジンです。
のび太君の知らない家(異なるオリジン):
一方で、のび太君が知らない別の家(Bくんの家)は町の遠くにあり、別の学校に通っています。のび太君は簡単には行けません。これが異なるオリジンです。
(映画とかで知らない人の家に不法侵入しまくってるけど行けないったら行けないんだってば!)
同一オリジンポリシーがあると何が起こるか
なぜ、このようなポリシー(規定)があるのでしょうか。これも、ドラえも⚫︎でイメージしてみたいと思います。
クッキーの制約:
のび⚫︎君が友達の家(同一オリジン)でおやつをもらえるように、ブラウザも同一オリジンのサイトに対してはクッキーを共有します。しかし、異なるオリジンのサイトではクッキーの共有ができません。これにより、ユーザーのプライバシーが保護されます。
安全なコンテンツの表示:
のび⚫︎たちの家(信頼されたサイト)でしか、のび⚫︎たちのおもちゃ箱(コンテンツ)は見せないようにしています。同一オリジンポリシーは、異なるオリジンからの埋め込みコンテンツ(例: < iframe>内のコンテンツ)へのアクセスを制限します。これにより、不正なサイトが信頼されたサイトのコンテンツを悪用することを防ぎます。
セキュリティの向上:
のび⚫︎君が知らない家(異なるオリジン)に行くとき、その家が安全であるかどうかを確認する必要があります。同様に、ブラウザも異なるオリジンからのリソースに対してはセキュリティ上の注意が必要です。同一オリジンポリシーは、クロスサイトスクリプティング(XSS)やクロスサイトリクエストフォージェリ(CSRF)などの攻撃からユーザーを保護する一助となります。
#### API呼び出しの制限:
のび⚫︎君が使うおもちゃ(API)も、同じ町のおもちゃ屋さん(同一オリジン)でしか買えないようにしています。異なる町のおもちゃ屋さんから買うには、許可証(CORS)が必要です。これにより、信頼されていないおもちゃ屋さんからの不正なおもちゃ(API呼び出しやデータの取得)を防いでいます。
ここまでで、「なんか同一オリジンポリシーって仕組みでセキュリティ守られてるっぽい」というのは伝わったかと思います(伝われ...!)
もう一つ大事なことは、「異なるオリジンでも許可すれば同一オリジンポリシーを無視できそう」ということです。何も知らずにエラーが出ているからと許可していると、思わぬ脆弱性が生まれそうです。
4.CSRF(Cross-Site Request Forgeries)
さて、長くなってしまいましたが本題のCSRFについてです。
CSRFが何で、Railsではどうよしなにこの対策をしてくれているのかを書いていきたいと思います。
CSRFってなに?
CSRFはクロスサイト・リクエスト・フォージェリと読み、主にセッション管理における特性を利用した脆弱性の一つです。
CSRFの脆弱性があると、下記のようなことが起こってしまいます。
・のび⚫︎が書いたラブレターをスネ⚫︎が勝手に書き換えてしまう
・ドラえも⚫︎のポケットからアイテムが勝手に取り出されて使われてしまう
・ドラえも⚫︎がパスワードを変更していないのに勝手にパスワードが変更されてしまう
※この章からは、のび⚫︎くんとドラえも⚫︎はHTTPやWebサーバではなく、ただのキャラクター名として使用しています(分かりづらくてすみません)
もう少しどんなことが起こるか具体的にみていきましょう。
①のび⚫︎がログインしているサイトAにアクセス:
のび⚫︎くんはラブレターを書くために、あるウェブサイトAにログインしています。このサイトAではセッション管理が行われ、のび⚫︎くんのログイン情報(セッション)がブラウザのクッキーに保存されています。
②悪意あるサイトに誘導:
スネ⚫︎「のび⚫︎のくせにラブレターなんか生意気だ!ちょっと懲らしめてやろう」
のび⚫︎くんは別のサイトBにアクセスします。そこではスネ⚫︎によって悪意あるCSRF攻撃が仕掛けられています。このサイトBは、のび⚫︎くんが無意識にアクセスするように誘導するために、魅力的なコンテンツやクリックボタンを用意しています。
のび⚫︎「わぁ!【絶対に成功するラブレターの書き方】だって!これでしず⚫︎ちゃんに想いを伝えられるぞ!(リンククリック)」
③ウェブサイトAへのリクエストを自動生成:
のび⚫︎くんが悪意あるサイトBにアクセスすると、サイトB内に仕込まれたスクリプトが自動的に、目標とするウェブサイトAへのリクエストを生成します。このリクエストには、サイトAで保存されたのび⚫︎くんのログイン情報(セッション)が含まれています。
④リクエストを実行:
のび⚫︎くんは悪意あるサイトBでの操作を続けます。
「なになに?『このボタンを押せば自動であなたにあったラブレターが作れます!』。なんて便利なんだ!(ポチポチ)」
その中にはサイトBで自動生成されたリクエストが、のび⚫︎くんがログインしているサイトAに対して送信される操作も入っていますが、偽装されているためのび⚫︎くんは気付きません。
⑤ラブレターの書き換え:
このサイトBからのリクエストは、彼のセッション情報が含まれているため有効となります。サイトAはその情報を信じ、まるでのび⚫︎くんが自分で書いたかのように、ラブレターを書き換えてしまいます。
⑥のび⚫︎の気づき:
のび⚫︎くんは何気なく自分のラブレターを見てみると、意図しない書き換えがされていることに気づきます。しかし、この操作がCSRF攻撃によるものだとは全く気づいていません。
このように、CSRFの脆弱性があると、自分の意志でない操作を強制されることになります。今回はラブレターでしたが、これが銀行やクレジットカードの取引、個人情報のやり取りなどであれば大変なことです。
「重要な処理」に対してのリクエストではCSRF対策としてリクエストが正当なものであるかを確認し、悪意あるサイトからの不正な操作を防ぐ仕組みを導入する必要があります。
RailsはCSRF対策をよしなにしてくれている
RailsではデフォルトでCSRF対策が組み込まれています。CSRF攻撃を防ぐために、Railsではセキュリティトークン(CSRFトークン)を使用します。
それが、form_with
などのフォームヘルパーです。
form_withを使うと、生成されるHTMLには隠しフィールドとしてCSRFトークンが埋め込まれます。このトークンはセッションごとに一意であり、フォームが送信されると一緒にサーバーに送信されます。
例えば、
<%= form_with do |form| %>
Form contents
<% end %>
と書くと以下のようなHTMLが生成されます。
<form accept-charset="UTF-8" action="/" method="post">
<input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
Form contents
</form>
<input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
の部分が、CSRFトークンが格納された隠しフィールドです。
サーバー側では、送信されたリクエストに含まれるCSRFトークンを検証します。このトークンが一致しない場合、リクエストは無効とされ、CSRF攻撃からアプリケーションが保護されます。
form_withを用いず、JavaScriptのみでPOSTリクエストを送らなければならないケースでは、CSRFトークンを取得してリクエストの中に含めなければエラーが出ます。
トークンを埋め込む方法以外に、パスワードの再入力やRefererのチェックといった対策方法がありますが、トークンを埋め込む方法がもっとも一般的だそうです。そして、フォームに専用のトークンを埋め込むことによるCSRF対策が成り立つのは、ブラウザに同一オリジンポリシーがあるからです。
まとめ
HTTPはステートレスな通信であり、ユーザーの情報(状態)を持たせるためにセッション管理をしている。しかし、ログイン状態などが維持されていると、セッション情報を悪用するCSRF攻撃によって、ユーザー情報を抜き取られたり、思わぬ操作を強制されることがある。その対策として、フォームに専用のトークンを埋め込むことによるCSRF対策があるが、それが成り立つのは、ブラウザに同一オリジンポリシーがあるからである。
(あってる?😇)
ここまで、色々と書いてきましたが、分かりやすく書くために削った部分もあり、特にGETやPOSTの違いなどセキュリティについて知っておくべきことは多いです。(私も勉強中です.....)
Railsガイドにどのようなセキュリティ対策が用意されているのか、何に気を付けるべきか記載されています。
また、通称「徳丸本」も読んでおくべき教科書だそうです。
最後に
何を隠そう対象読者の「Railsの<%= form_with do |f| %>
などを「HTMLのままでもいいじゃん...」と思っている人」とは、カリキュラム中の私でした。
入門編のWeb技術入門も「読んでて眠い」としか思えなかったし、興味も持てなかったです。
しかし、カリキュラム受講中はよく分からなかった言葉が、Webアプリを作ったことで「あっ! あの時のCan't verify CSRF token authenticity.
ってエラーはこれのことか」とか、「APIがHTTPSじゃないと使えない仕様なのはそういうことか」と分かることも増えてくると、Web技術入門もイメージできることが増えて前より興味深く読むことができました。
今後、エンジニアになるために大切な知識だと思うので、また記事を書ければと思います。その際はぜひお付き合いください。
訂正などありましたら、コメントにぜひお願いします🙏