読書メモ:安全な Web アプリケーションの作り方(第2版)

読書メモ:安全な Web アプリケーションの作り方(第2版)

August 27, 2023

長いこと積読状態にあったが、最近セキュリティについて勉強しようと思い立ちようやく読了。セキュリティ界隈では著名な徳丸さんによる本で、本書は「徳丸本」と呼ばれることも多い。

内容の一部を以下にメモしておく。


CSRF(Cross-Site Request Forgeries:クロスサイト・リクエストフォージェリ) #

ウェブサイトの中には、サービスの提供に際しログイン機能を設けているものがあります。ここで、ログインした利用者からのリクエストについて、その利用者が意図したリクエストであるかどうかを識別する仕組みを持たないウェブサイトは、外部サイトを経由した悪意のあるリクエストを受け入れてしまう場合があります。このようなウェブサイトにログインした利用者は、悪意のある人が用意した罠により、利用者が予期しない処理を実行させられてしまう可能性があります。このような問題を「CSRF(Cross-Site Request Forgeries/クロスサイト・リクエスト・フォージェリ)の脆弱性」と呼び、これを悪用した攻撃を、「CSRF 攻撃」と呼びます。

https://www.ipa.go.jp/security/vuln/websecurity/csrf.html

自サイト上にアクセスすると副作用を発生させるエンドポイントがあると仮定する。通常の操作であれば、自サイト内のフォームを送信することでそのエンドポイントにアクセスすることを想定しているが、悪意のある外部サイトからそのエンドポイントへと望まないリクエストを飛ばすのが CSRF である。

たとえば、自サイトがショッピングサイトとした場合、悪意のある攻撃者は、自サイトの会員を悪意のある外部サイトへと誘導したのち、その外部サイトから会員本人の Cookie を付与しつつ自サイトに対して注文処理を発行する。これにより会員本人が意図していない商品購入を実行させられてしまうことになる。

被る悪影響は、自サイトのユーザ本人が意図していない副作用を発生させられること。ただし、ユーザの個人情報などを盗むなどといったことはできない。

外部サイトから自サイトにリクエストを送る方法としては、主にリンクによる GET リクエストと、フォームによる POST リクエストがある。なお基本的な考え方として、そもそも副作用を生じさせるエンドポイントは GET ではなく POST で提供するべきである。たとえばログアウト処理の場合であっても、それはユーザのセッション状態の変更という副作用を生じさせるものであるから、POST のエンドポイントとして用意する。そのため、ここでは主に POST リクエストによって CSRF が試みられることを前提に考える。

この脆弱性が生まれる原因は主に次の2点からなる。

  • form 要素の action 属性は、その仕様として、どのドメインの URL でも指定することができる。
  • 対象のサイトにアクセスするとき、外部サイトからのリクエストであっても、対象のサイトの Cookie はリクエストに自動的に付与される。

ログイン中のユーザは、そのセッション ID やトークンのようなものを Cookie に保管し、リクエスト時にはサーバサイドで Cookie の情報を確認したのちにリクエストの正当性を検証することが多い。悪意のある外部サイトからのリクエストであっても、自サイトで発行した正当な Cookie が付与された状態でリクエストが渡ってくるため、サーバサイドとしては自サイトからリクエストが発行された時と同様に処理を行ってしまうことで、ユーザ本人にとっては意図しない副作用が実行されてしまう。

対策はいくつかある。

  • 自サイトからのリクエストであることを確認する。
  • 正規利用者の意図したリクエストであることを確認する。
    • パスワードの再入力
    • CSRF トークンの利用
  • 外部サイトからのリクエストに対しては Cookie を付与しないようにする。

「自サイトからのリクエストであることを確認する」 には、HTTP リクエストヘッダーの Referer をチェックすればよい。どのサイト上から発行されたリクエストなのかを確認し、自サイト上で発生したものでなければ処理を行わないようにする。ただしこの方法には考慮しなければならない点がある。ユーザはブラウザの設定を変えることで、Referer 情報を送信しないように設定することができる。こうした設定を利用しているユーザは、自サイトからの正当なリクエストであっても、Referer が付与されていないため、サービス利用が事実上不可能になってしまう。

「正規利用者の意図したリクエストであることを確認する」 方法は、Cookie 以外の情報も組み合わせて正当性を検証することで対策をするものである。CSRF が成立するのは、ユーザの認証を Cookie のみに頼っているためである。

その1つは、パスワードの再入力を要求することである。重要な処理を実行する画面では、あわせてユーザのパスワード入力も要求し、リクエストにはそのパスワードも含めて送信する。サーバサイドではパスワードの正当性を検証し、問題がなければ処理を進める。悪意のある外部サイトが CSRF を試みた場合、ユーザのパスワードをリクエストに含めなければいけないが、それがわからないため攻撃は成立しなくなる。一方で、ユーザからするとパスワードの入力を求められることにより操作の負荷が高まるため、この方法をとって良いかは UX の観点から検討が必要である。

2つ目の方法は、CSRF トークンを扱う方法である。具体的には、そのページを開く際にサーバサイドで乱数(ここでは CSRF トークンと呼んでいる)を発行したうえで、ページの input タグに hidden で値を埋め込んでおく。リクエスト受付時に、先の CSRF トークンと同じものが送られてきていることが確認できれば、ユーザが自サイト上で発行した正当なリクエストであることが確認できる。なお、現代のクライアントサイドレンダリング型のアプリであれば、ログイン処理とあわせて CSRF トークンを発行しておき、クライアント側ではメモリ上にトークンを保管、以降リクエスト時にそれを付与するといった方法もとられる。この場合には慣習として、HTTP リクエストヘッダーに X-CSRF-Token のフィールドを設けて、そこにトークンを設定してリクエストを発行する形を取ることが多い。詳細は以下を参照のこと。

「外部サイトからのリクエストに対しては Cookie を付与しないようにする」 方法は、Cookie に SameSite=Strict または SameSite=Lax を設定するものである。SameSite 属性は CSRF 対策として策定されたもので比較的新しい仕様である。通常のサイトであれば Lax が使われることが多い。特に指定しなかった場合のデフォルト値も Lax である。Strict と Lax の違いは以下を参照のこと。

なお、CSRF の保険的な対策として、重要な操作のあとにはユーザにメールなどの通知を行うこともあげられる。 攻撃を防ぐことはできないが、万が一攻撃を受けた際にユーザ本人が早期に気づくことができ、被害を最小限にとどめることができる可能性がある。

補足:Web API における CSRF #

Ajax などでアクセスされる Web API においても CSRF の脆弱性は同じだが、CORS の仕様によって未然に防がれていることも多い。(ただしいずれにせよ、CORS の仕様で結果的に CSRF を防御するのではなく、先に挙げた本質的な CSRF の対策を取り入れるべきである。)

CORS の仕様に従って、単純なリクエストの場合はプリフライトリクエストが発生しないが、そうでないリクエストの場合はプリフライトリクエストが発生する。詳細は以下を参照のこと。

Web API では JSON を用いてやりとりすることが主流であるため、Content-Type ヘッダーに application/json を設定していると想定すると、この場合、単純なリクエストに該当しなくなりプリフライトリクエストが発生するやり取りとなる。

CORS ではサーバ側でアクセスを許可して良いオリジンを Access-Control-Allow-Origin で指定するが、プリフライトの際に Access-Control-Allow-Origin のチェックにて対象外のドメインと判断されることで、プリフライトに続くメインのリクエストが拒否される(送信されない)ことで、CORS は未然に防がれる。

なお、単純なリクエストに該当している場合についても、同様に Access-Control-Allow-Origin のチェックに引っかかり、サーバからクライアントにエラーが返る。ただし、サーバ側にリクエスト自体は到達していることから、そのままサーバ上で処理が行われ、結果として副作用のある処理は実施されてしまう。

そのため、Web API において、CORS が未然に防がれている状態に該当するのは、プリフライトが要求されるエンドポイントであって、かつ Access-Control-Allow-Origin が自ドメインだけに設定されている(Access-Control-Allow-Origin: * ではない)場合である。

サーバサイドで Content-Type: application/json を要求・検証すること

Web API として JSON を受け取る前提としていながら、クライアントからのリクエストヘッダーに Content-Type: application/json を要求しない、検証しない形になっている Web API はかなり多い。

この場合、クライアントは Content-Type: application/json を指定しなくともリクエストができる状態となっているため、プリフライトが発生しない単純なリクエストとしてアクセスすることも可能となり、結果として CORS の脆弱性が露見する可能性もある。

JSON を前提としている場合は、サーバサイドで受け取ったリクエストの Content-Type を忘れず検証することで、クライアントサイドにその設定を強制する仕組みとすること。

補足:ローカルストレージ & JWT のセッション管理の場合 #

セッションにまつわる情報を Cookie で管理する伝統的な方法に加えて、最近はローカルストレージに JWT などのトークンを格納することで管理する方法も一般的となっている。Cookie とは異なり、この場合だと外部ドメインからのリクエストであってもその情報が自動で付与されることはないため、セッション情報を必要とするような CSRF は成立し得ない。

クリックジャッキング #

ウェブサイトの中には、ログイン機能を設け、ログインしている利用者のみが使用可能な機能を提供しているものがあります。該当する機能がマウス操作のみで使用可能な場合、細工された外部サイトを閲覧し操作することにより、利用者が誤操作し、意図しない機能を実行させられる可能性があります。このような問題を「クリックジャッキングの脆弱性」と呼び、問題を悪用した攻撃を、「クリックジャッキング攻撃」と呼びます。

https://www.ipa.go.jp/security/vuln/websecurity/clickjacking.html

被る悪影響は CSRF と同様。ユーザ本人が意図していない副作用を発生させられること。ユーザの個人情報などを盗むなどといったことはできない。

手法としては、悪意のあるサイトにユーザを誘導した後、iframe 要素を使って攻撃対象のサイトを表示する。その上に、関係のないページ(X とする)を重ねるように表示し操作させることで、あたかもユーザにとっては X を操作しているように見せながら、iframe に表示されているものを操作させるという方法である。

意図しないサイトで iframe による自サイトの表示を許可しないことが対策となる。これは HTTP レスポンスヘッダーに X-Frame-Options を設定することで実現できる。そこから現在は仕様が変わり現在はレスポンスヘッダーの Content-Security-Policy の frame-ancestors で設定する形に移り変わりつつある。

オープンリダイレクト #

オープンリダイレクトとは、Web アプリケーション内の他ページや外部サイトに遷移するためのリダイレクト機能を悪用し、被害者を攻撃者が用意したページに誘導することが可能になる脆弱性です。

以下のようにクエリストリングでリダイレクト先の URL を渡すことで、指定された URL に遷移が発生する実装がよく見られます。 https://example.co.jp/?redirect=(リダイレクト先URL)

上記の機能を提供する Web アプリケーションがオープンリダイレクトに脆弱な場合、以下の図のように攻撃者が変更した URL を被害者にアクセスさせることで攻撃者の用意したページに誘導することが可能になります。 https://example.co.jp/?redirect=(攻撃者が用意したページのURL)

https://www.mbsd.jp/research/20220526/open-redirect/

攻撃者はあらかじめ https://example.co.jp/?redirect=(攻撃者が用意したページのURL) といった悪意のあるリダイレクト先を指定した URL を作成した上で、メールなどでこれを配布し、利用者をリダイレクト先に誘導する。

リダイレクト前のページである example.co.jp は利用者が信頼しているドメインであるため、利用者は自分が信頼するサイトを閲覧しているつもりのまま、知らないうちに悪意のあるサイトに誘導される。リダイレクト先にはもとのサイトに似せたデザインのサイトが用意されており、そこでユーザ情報などを入力させるといった悪事が試みられることになる。

対策としては、「リダイレクト先の URL を動的にせず、必ず固定にする」「リダイレクト先の URL を直接指定せず、対応するリダイレクトごとに番号指定にする」であったり、「リダイレクト先のドメイン名をチェックする」といったものがある。外部から与えられた URL をそのまま使用しているのが原因となるため、それを行わないのが前者群の対応であり、URL の検証を徹底するのが後者の対応である。後者の場合、リダイレクト先が自サイトのドメイン配下のページであることを正規表現で確認すれば良い。

パスワード認証を狙った攻撃への対策 #

ログイン試行の攻撃

オンラインでのブルートフォース攻撃への対抗策としては、アカウントロックが有効。パスワードの間違いが一定回数を超えた場合にアカウントをロックする。たとえば、パスワードを 10 回間違えた際は、そのアカウントを 30 分間ロックするというのが考えられる。正常にログイン成功した場合は、間違いのカウンタをリセットする。ロックされた場合は、30 分後にロックの自動解除およびカウンタをリセットする。

30 分だと短いと感じるかもしれないが、これでも相当の効果がある。上述の通り 10 回/30 分 の制限とした場合、攻撃者が 100 個のパスワードを試すのに 4 時間半以上かかり、10 回のアカウントロックが発生する。この際に開発者に通知を行うようにしておけば、アカウントロックの状況を調べた上で、必要に応じて攻撃元の IP アドレスからの通信を遮断するなど、対処の時間を確保することができる。なお、クレジットカード加盟店向けのセキュリティスタンダードである PCI DSS 3.2 の基準でもロック時間は 30 分とされている。

リバースブルートフォース攻撃といったものがある。これはブルートフォース攻撃とは逆に、パスワードを固定しつつ、ユーザ ID を入れ替えながらログインを試行するものである。さらに両方を入れ替えながら試行する、パスワードスプレー攻撃もある。さらに、別サイトで流出した ID/パスワードリストを用いてログイン試行する、パスワードリスト攻撃もあり、こうした攻撃にはアカウントロックの対抗策は無力である。こういった発展的な攻撃に対しては、二段階認証を用いなければ対抗できない。

パスワード解析の攻撃

パスワードはハッシュ化して記録することが前提だが、その場合でもさまざまな攻撃が発生している。

ある文字列に対してある暗号化アルゴリズムを用いてハッシュ化した場合、結果は必ず同じものとなる。

よく使われるパスワードのハッシュ値をあらかじめ総当たりで算出しておいて、該当するハッシュ値から逆引きでパスワードを求めようとする手法が、レインボーテーブルである。

こういった攻撃の対策として、ソルトが利用される。ソルト(salt)は、ハッシュの元データに追加する文字列を指す。ソルトにより、見かけのパスワードを長くするとともに、ソルトをユーザごとに異なるものにすることで、パスワードが同じでも異なるハッシュ値を生成する。

ソルトを使ってもブルートフォース攻撃の脅威は残っている。ソルトを使っても、ハッシュの計算時間はさほど変わらないためであり、ブルートフォース攻撃に対抗するためには、ハッシュ計算の速度を遅くする必要がある。そのためには、ハッシュ計算を繰り返し行うことで時間をかけるストレッチングと呼ばれる対策と、パスワードハッシュ化用の遅いハッシュ関数を用いる対策がある。後者の具体的なハッシュ関数の1つに BCrypt がある。