30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

SPA の CSRF 対策や CORS について検証する

SPAのCSRF対策について調べる機会があったので、まとめておく。
CORSの挙動を中心に、実際にコードを書いて検証していった。
間違ったことは書いていないと思うけど、利用は自己責任でお願いします。

CSRF とは

CSRFそのものについての詳しい記事はネット上にいくらでもあるので、そちらを参照。

一例:第3回 Webセキュリティのおさらい その3 CSRF・オープンリダイレクト・クリックジャッキング:JavaScriptセキュリティの基礎知識|gihyo.jp … 技術評論社

一言で言ってしまえば、サービスの利用者を罠サイトにアクセスさせて、APIサーバーに対する意図しないリクエストを送信させることである。
そのユーザーがサービスにログイン済みであり、セッションIDをクッキーに保存していた場合、その情報も一緒に送信される。そのため、そのリクエストは「認証済みのユーザーからのリクエスト」であるため、ユーザーが意図していなかったとしても、そのリクエストは正当なリクエストとして処理されてしまう。
これにより、退会や決済、SNSへの不適切な投稿などのセンシティブな操作が、ユーザーが意図しない形で行われてしまう。

CSRFトーク

CSRF対策としてよく利用されるのが、トークンを用いた対策。だがこれは、SPAでは難しい。
SPAをレンダリングするサーバーとAPIサーバーが別々の場合、どうやってトークンを受け渡しするのかが自分には分からなかった。

例えば Rails アプリの場合は、Rails が返す html のなかにトークンを仕込んでおき、それをdocument.getElementsByName('csrf-token')[0].contentのような形で拾って、それをAPIへのリクエストの際に送信すればよい。
だが一般的なSPAではフロントエンドとバックエンドが独立して存在していると思うので、この方法は使えないと思う。

CORS(Cross-Origin Resource Sharing)

SPAでAPIを叩く場合は、formではなくXMLHttpRequestsfetchを用いて非同期通信で叩くことになるのが一般的だと思うが、異なるオリジンに対して非同期通信を行うときは必ず、CORSを用いた通信になる。
このCORSの仕組みが、「トークンを使わない場合のCSRF対策」の根幹になる。

本稿では、APIサーバーと同じオリジンからのリクエストについては、「信頼できるリクエスト」と見做すことにする。
XSS脆弱性でもない限り、CSRFは異なるオリジンから行われるので、この前提はおかしくないと思う。
そして当然、XSS対策はCSRF対策とはまた別にそれ自体が行われるべきなので、CSRF対策を扱う本稿ではスコープ外となる。

そのため、CSRFによるリクエストは、次の2種類になる。

  • 異なるオリジン間の非同期通信
  • 異なるオリジン間の同期通信

上述したように、「異なるオリジン間の非同期通信」は必ずCORSを利用した通信になるので、CORSの仕組みを利用してCSRF対策を行っていく。

formなどの、同期通信によるリクエストへの対策については、後述する。

Access-Control-Allow-Origin の落とし穴

CORSではAccess-Control-Allow-Originというレスポンスヘッダを使って、アクセスを許可するオリジンをサーバー側が指定する。
これを使うことで、信頼できるオリジン以外からのリクエスト、つまり、CSRF攻撃を仕掛けてくるオリジンからのリクエストを制限することが出来る。

しかしAccess-Control-Allow-Originさえ使っていれば安全かといえば、そうではない。落とし穴がある。
CORSは「Cross-Origin Resource Sharing」の名の通り、あくまでもリソースの読み込みを制御するというだけで、たとえ許可していないオリジンからであったとしてもサーバーへのアクセス自体は発生するのである。
そのため、サーバーサイドの実装によってはCSRF攻撃が成立してしまう。

実際に再現してみる。

本稿で紹介する事象は全てコードで検証しており、そのコードはGitHubで公開してある。

github.com

10.9.0以上のNode.jsと1.12.3以上のYarnがあれば動く。
$ yarn startで、Vueで作られた「マイページ」が開く。

早速、先程の脆弱性を再現してみる。

まずは説明文に従ってログインする。その後、Buyボタンを何回か押して、購入する。ブラウザをリロードしても「購入数」に変化はないことを確認する。

次に、ページ下部のリンクで「罠ページ」に遷移してからthrow simple cross-origin requestと書かれているボタンを押す。
すると、許可していないオリジンからの非同期通信なので、この通信はレスポンスを得られない。
だが、APIへのアクセス自体は発生してしまっており、マイページに戻ってリロードすると、「購入数」が0になっていることを確認できる。

つまり、Access-Control-Allow-Originを指定しただけではCSRF対策としては不十分だということが分かる。

プリフライトリクエス

上記のような攻撃を防ぐために、プリフライトリクエストを利用する。

プリフライトリクエストとは、CORSの仕組みの一部で、特定の条件においてブラウザが自動的に発行するリクエストのこと。IE11を含め、メジャーなブラウザは全て対応している。

具体的には、本来のリクエストを送ることが許可されているかを確認するためにまず、OPTIONSメソッドでリクエストを投げ、許可されていた場合にのみ、続けて本来のリクエストを投げる。

どのようなリクエストを許可するかは、Access-Control-Allow-*という形式のレスポンスヘッダを使って条件を指定する。Access-Control-Allow-Originでオリジンを指定したように、許可するリクエストヘッダやHTTPメソッドを指定できる。
ただ、Access-Control-Allow-Methodsとは無関係に、GETPOSTは許可されているようだった。DELETEは明示的に許可していないとエラーになる。ブラウザによって異なるかもしれない。ここらへんの挙動は詳しく調べていないので分からない。

事前に必ずOPTIONSメソッドによるリクエストが発生する、というのが重要であり、これにより、CSRF攻撃のリクエストはAPIサーバーに届かなくなる。許可していないオリジンからのアクセスということで、その前のプリフライトリクエストで通信が終了してしまうから。

プリフライトリクエストを発生させる方法はいくつかあるが、リクエストに独自ヘッダをつければ、必ず発生する。

そのため、APIサーバーへのリクエストには何らかの独自ヘッダの付与を義務付け、そのヘッダがないリクエストは全て拒否することで、CSRF対策になる。

独自ヘッダがついていなければそれが理由でエラーになる。
ついていた場合は、プリフライトリクエストが発生するため、許可していないオリジンからのリクエストは事前に失敗する。

これも、先程のリポジトリで再現できる。

  1. 最初に開いていたマイページの画面上部のリンクから、「CSRF対策が行われているAPIを利用しているマイページ」に移動する
  2. 先程の要領でログインし、「購入数」を適当に増やしておく
  3. 画面下部のリンクから「罠ページ」に遷移する
  4. 「throw preflight cross-origin request」を押下すると、独自ヘッダを付与したCSRF攻撃を仕掛ける
  5. 「throw simple cross-origin request」を押下すると、独自ヘッダを付与していないCSRF攻撃を仕掛ける
  6. 45のいずれの操作を行っても攻撃に失敗し、「購入数」に変化がないことを確認する

同期通信による CSRF への対策

ここまで書いてきたのは、CORSを利用したセキュリティ対策。
しかし、formによる送信などの同期通信はCORSの対象外となってしまい、オリジンによる制限などを利用できない。
そのため、formを使ってCSRF攻撃を行われた場合は、無防備になってしまう。

CSRF対策が十分に行われていないAPIへの攻撃ページ」の「お得な情報はこちらから」がまさにそれで、このリンクをログイン済みのユーザーにクリックさせて、CSRF攻撃を行う。
しかもこのケースではiframeを使っているため攻撃後の画面のリロードが行われず、ユーザーは気付きにくい。

これを防ぐためには、何でもいいので独自ヘッダを義務付けるようにすればよい。
独自ヘッダを付与するのはformなどでは不可能で、XMLHttpRequestsfetchを使わなければならない。
そのため、formなどの同期通信によるリクエストを防ぐことが出来る。

つまり、独自ヘッダを義務付けることで、プリフライトリクエストを発生させるだけでなく、同期通信によるCSRF攻撃を防ぐ効果も生まれるのである。

この仕組みにより、「CSRF対策が行われているAPIへの攻撃ページ」の「お得な情報はこちらから」による攻撃は失敗する。

ただ、XMLHttpRequestsfetch以外では独自ヘッダを付与できないことを明示している資料は見つからなかった。誰か知っていたら教えてください。

認証情報をクライアントのどこに保存するか

ここまでの説明で、CSRF対策としては十分だと認識している。
ただ、認証情報をどこに保存するのか、という問題にも軽く触れておく。

ここでいう認証情報とは、セッションIDやトークンなどの、そのユーザーであることを証明するための情報。これがあることで、サーバー側に認証・認可してもらえる。

保存場所の選択肢は2つ。まずはCookie。かつてはこれしか選択肢はなかった。だが現在では、Web Storageという仕組みもある。

Cookieの利点は、HttpOnly属性を有効にすることで、JavaScriptから読み込まれることを禁止できること。これにより、XSS脆弱性があったとしても、そこからCookieを読み込まれる心配がない。
また、認証に使うことが一般的であり、そのためのノウハウやライブラリが豊富にある、というのも利点だと思う。
欠点は、リクエストを送る度に自動的にCookieの情報も一緒に送信されてしまうこと。例えばhttp://example.comから渡されたCookieは、http://example.comにリクエストを送る度に自動的に送信されてしまう。多くのCSRFは、この仕組みを悪用したものだと言える。

Web Storageの利点は、Cookieと違い、明示しない限りサーバーに送信されることはないということ。
そして、同一生成元ポリシーに基づきWeb Storageはオリジン毎に管理されているため、「罠ページ」で、Web Storageを読み込まれてしまうことはない。
この2つの特徴により、認証情報をWeb Storageに保存しておけば、それだけでCSRFを防げるはず。
欠点は、同じオリジンであれば簡単にJavaScriptで読み込めるため、XSS脆弱性があった場合はWeb Storageの中身を読み取られてしまう。
また、Cookieに比べれば、認証に用いることがまだ一般的ではなく、サーバーサイドの実装で工数が増えてしまうかもしれない。
さらに、比較的新しい技術なので、一部のブラウザでは上手く機能しない恐れがある。例えば、v10までのSafariは、プライベートブラウジングモードではWeb Storageが上手く機能しなかったAppleの開発者のツイートによれば、v11から修正されている。

まとめ

  1. リクエストヘッダに何らかの独自ヘッダを加えることを義務付け、それがあるかをまずチェックして、存在しなければそのリクエストは不正なアクセスだと見做す
  2. Access-Control-Allow-*でどのようなオリジン間通信を許可するのかを正しく設定する

この2つの対策を行うことで、SPAのCSRF対策としては十分だと思う。
ただ、既に述べたように、XSSなどでAPIサーバーと同じオリジンからCSRFを行われた場合は、この限りではない。

参考資料

「『現場で使える Ruby on Rails 5速習実践ガイド』増刷記念 著者交流会」に行ってきた

『現場で使える Ruby on Rails 5速習実践ガイド』の著者にサインをもらえるイベントがあったので、行ってきた。

diveintocode.doorkeeper.jp

正月休みに読んだのだが、よい本だった。本書のおかげで少しは「Railsの基礎を身に着けた」と思えるようになった。

よい本だったので、その著者にサインをもらえると知り、行ってみようという気持ちになった。
主催は DIVE INTO CODE というプログラミングスクールで、これからエンジニア業界を目指す人向け、という意味合いが強いイベントらしい。
なので自分は想定される参加者とは少し違うが、「Rails は初心者なのだから似たようなもんだろう」という気持ちで参加した。席も余裕があったし。
自己紹介のときにも話したのだが、かなりミーハーな気持ちで参加した。そうそうない機会だろうから。

まずは櫻井さんの講演から。

本書の出版の経緯と、Rails の学習方針について。

CURD ができた、アプリができた、というのは通過点に過ぎず、現実の問題を解決してこそ「Rails を使えている」と言える、というのは本当にそう思う。
導入として簡単な掲示板やブログを作ってみることに意味はあるはずだけど、それは、「現実的なコード」「実用的なコード」からは程遠い。
自分も1年独学してから就職したが、現場や実践でないと学びにくいことはたくさんあると実感した。

上手くなるためには訓練が必要なわけだが、具体的にはどういう風に訓練していくべきなのか。
裏側に思いを馳せる、というアプローチをお勧めされていた。
具体的には Rails の実装を読んだり、gem の中身を調べたり、ActiveRecord でどのような SQL が発行されているのかを見たり。
広く使われているライブラリを読むことで勉強になる、みたいなのは確かによく聞く。
ライブラリではなく「Rails アプリ」なら、Mastodon とかがいいのだろうか。

それこそ、DIVE INTO CODE のようなプログラミングスクールを使うのもいいのだろう。
詳しくないのだが、櫻井さんとペアプロをしたり出来るらしい。
これはかなり魅力的。

自分の能力を効率的に上げるためには、自分よりも圧倒的に優れている人と働くのが重要だから。
自分と同じくらいとか、ちょっと上とかではなく、圧倒的に上。

講演の後にあった質問コーナーでも、すごい人と働くにはどうすればいいか、という趣旨の質問をした。

すごい人と働くことの効能は、身をもって知っている。
このブログで一番はてなブックマークを集めている以下の記事は、すごい人から学んだ内容をまとめたものだ。

numb86-tech.hatenablog.com

キャリアの最初にこの方に鍛えてもらったことで相当勉強になったし、プログラマとしての正しい姿を見ることが出来た。この方がいるから入社したみたいなところがあり、それは本当に正解だった。とはいえ、そういう理由で入社するとその方がすぐに退職したときに厳しさがある。

他のプログラマの方も、一緒に働くプログラマや環境の重要性を述べている。

diary.shuichi.tech yshibata.blog.so-net.ne.jp

質問に対するご回答。

  • 櫻井さん
    • アンテナを高く張って、イベントとかで会社の存在を知る
    • GitHubですごい人のソースコードを見る
  • 大場さん
    • すごい人と働くのは現実的には難しい
    • 自分がすごくなるしかない
      • 狭くてもいいので、自分が得意な分野を作るのがよい
  • 松本さん
    • 技術コミュニティに入ることで、すごい人たちと接点を持てる
    • 思いがけず、自分の周囲にすごい人がいたりする
      • よちよち.rb というコミュニティに参加していたとき、Ruby は初心者だが他の分野ではすごい人、という方がいた
  • 小芝さん
    • 一緒に「働く」ということに拘る必要はなく、コミュニティというものもある
    • コミュニティで発表したり、何かを与えたりすることが大事

王道や近道はない、という感じだろうか。

RubyRails の腕を磨いてそれを武器に環境を移っていく、というのは現実的ではない気がするので、フロントエンドを武器にすればいいんだろうか。

取り敢えず、React でも頑張ろうかという気持ちになっている。
折しもこの日、v16.8がリリースされて、React Hooksが正式に導入された。
今の職場は Vue を使っているが、自分の動き次第で React を使った SPA の新規開発をやれそうな気配が生まれているので、それをやろうかと思っている。

reactjs.org

サインも無事にもらえた。