🛡️

APIにおけるCSRFについて【パターン解説】

2023/04/10に公開

概要

API(Ajax)リクエストにおけるCSRF(クロスサイトリクエストフォージェリ)について、リクエストの認可方法あるいはCORS設定毎にパターン試行したメモ。

基本事項

  • ブラウザはChromeで検証
  • POSTメソッド+JSONボディのリクエストを想定する(Content-TypeがJSONとは限らない)
  • 別オリジン(www.google.com など)で開発者ツール>consoleでJavaScriptを実行することで、クロスオリジンなリクエストを再現する
  • APIはAWS APIGatewayで作成し、リクエストが到達したかを確認するためにCloudWatchにログを吐くよう構築 ※APIのドメインはデプロイ時に払いだされたものをそのまま利用

用語の省略

以下のように用語を省略します

この記事での用語の省略

以下はレスポンスヘッダ

  • ACAO:Access-Control-Allow-Origin
  • ACAH:Access-Control-Allow-Headers
  • ACAC:Access-Control-Allow-Credentials

1.サーバ側でリクエスト認可なし&CORS設定無

詳細

  • APIを叩くための認可(CookieやAuthorizationヘッダにアクセストークン)は特に必要ない
  • CORSの設定無(=最も堅牢な設定ともいえる)

結論

サーバが単純リクエストを受け付けてしまう場合、CSRF可能。一例として以下のようなJSが実行されるウェブページを被害者が踏む。(urlがCSRF脆弱なシステムとする)

//メソッド、URL、JSONボディパラメータ、ヘッダーの作成
let method = "POST";
let url = "https://udekc8lgcf.execute-api.ap-northeast-1.amazonaws.com/stage1/apicsrftest1";
let body = JSON.stringify({test: "csrf1"});

//リクエストを送信
fetch(url, {method, body})

ブラウザではCORSエラーが表示されている(レスポンスが読み込めない)

しかし、サーバーにはリクエストが到達している(CloudWatchのログ)。もしリクエストがコミット処理の場合、ブラウザでレスポンスは確認できなくとも処理が実行されてしまう可能性があるということ。

逆にサーバが単純リクエストを受け付けない(Content-Typeヘッダがapplication/jsonの時のみ受け付けるなど)場合はプリフライト飛ぶ→CORS設定ないのでブラウザのSOPによって後続のPOSTリクエストが飛ばないため、CSRFが防がれる。

補足

「サーバが単純リクエストを受け付けてしまう場合」について、主に以下のような例が考えられる

  • サーバにはリクエストのカスタムヘッダが存在するか否かという検証がない
  • パラメータを下記いずれかの形式で受け付けてしまう
    1. JSONで受け付けるがContent-Typeヘッダはapplication/jsonでなくてもいい
    2. Content-Typeヘッダはapplication/x-www-form-urlencodedでパラメータの形式もそれに合わせた形式でいい
    3. Content-Typeヘッダはmultipart/form-dataでパラメータの形式もそれに合わせた形式でいい
    4. そもそもパラメータ必要なく、Content-Typeヘッダはtext/plainでいい

※上記はあくまで一例であることに注意

2.サーバ側でリクエスト認可なし&ACAO設定有

詳細

  • APIを叩くための認可(CookieやAuthorizationヘッダにアクセストークン)は特に必要ない
  • ACAO:許可したいオリジンのみ

結論

1.と同じように、サーバが単純リクエストを受け付けてしまう場合にはCSRF可能。
※サーバが単純なリクエストを受け付けてしまうのであれば、CORS設定有無はCSRF可能・不可能に影響しない

3.サーバ側ではリクエストをCookieによる認可&ACAO設定有&ACAC設定有

詳細

  • APIを叩くためにCookie(値は十分にランダムであるという想定)が必要
  • ACAO:許可したいオリジンのみ
  • ACAC:trueの設定あり

すなわちプリフライトでは以下のようなレスポンスが返ってくる

結論

サーバが単純リクエストを受け付けてしまう、かつCookieのSameSite属性がnoneの場合にCSRF可能。

具体的には以下のようなJSが実行されるウェブページを被害者に踏む。(credentials:'include'はなくてもブラウザが勝手にCookieつけるかも。単純リクエストであることがキモ)

//メソッド、URL、JSONボディパラメータ、ヘッダーの作成
let method = "POST";
let url = "https://udekc8lgcf.execute-api.ap-northeast-1.amazonaws.com/stage1/apicsrftest3";
let body = JSON.stringify({test: "csrf3"});
let headers = {"Content-Type": "application/x-www-form-urlencoded"};

//リクエストを送信
fetch(url, {method, headers, body, credentials:'include'})

対象ドメインのCookieのSameSite属性がLax場合

ChromeのCookie設定

別ドメインから単純リクエスト送信(ConsoleでJS実行)

Networkタブに移動し、リクエストヘッダを確認→Cookieついていない

対象ドメインのCookieのSameSite属性がNoneの場合

ChromeのCookie設定

別ドメインから単純リクエスト送信(ConsoleでJS実行)

Networkタブに移動し、リクエストヘッダを確認→Cookieついてる

AWS ClowdWatchでも(APIGatewayに)リクエストが到達していることを確認

補足

結論部分ではJSにより単純リクエストを再現していたが、以下のようなHTMLによるリクエストと同じようにSameSite属性によるCookie制御と同じになるということ(ACACが無関係になる)。

要は「単純リクエストを正規のAPIとして誤って処理しないよう、サーバ側でプリフライト強制させるようなリクエストに限定して処理を受け付けるようにしろよ」ということである。

<form method="post" action="https://udekc8lgcf.execute-api.ap-northeast-1.amazonaws.com/stage1/apicsrftest3">
<input name="test" value="csrf3">
<input type="submit" value="Submit">
</form>

GETメソッドでコミット処理している場合はSameSite属性がLaxでもCSRF可能の可能性があるため、詳細は省くがコミット処理はPOSTメソッドでやるべき。知りたければtop-level navigationを検索(以下は参照例)
https://qiita.com/muk-ai/items/10aab285784e780ef631
https://zenn.dev/chot/articles/53f99fbb7ca77f

4.サーバ側ではリクエストをCookieによる認可&ACAO設定有(*)&ACAC設定有

詳細

  • APIを叩くためにCookie(値は十分にランダムであるという想定)が必要
  • ACAO:*(すべてのオリジン)
  • ACAC:trueの設定あり

すなわちプリフライトでは以下のようなレスポンスが返ってくる

結論

CSRFできない。
というより主要なブラウザはACAOが*の場合、ACACがtrueでもJSからCookieが付与できないようになっている。以下はプリフライトで「ACACが*の時にJSのcredentials=includeモードでCookie付与しようとするな!」と怒られている例。

もしCookieを付与してしまうブラウザがあれば、CSRF可能になると思われる。

5.サーバ側ではリクエストをAuthorizationヘッダ値で認可&CORS設定無

詳細

  • APIを叩くためにAuthorization(値は十分にランダムであるという想定)が必要
  • CORSの設定無(=最も堅牢な設定ともいえる)

結論

CSRFできない。
Authorizationヘッダが必要=プリフライト飛ぶが、CORSの設定が一切ないのでブラウザで次リクエストが遮断される。

試しに以下のJSをクロスオリジンでAPIエンドポイントに向けて実行してみる

//メソッド、URL、JSONボディパラメータ、ヘッダーの作成
let method = "POST";
let url = "https://udekc8lgcf.execute-api.ap-northeast-1.amazonaws.com/stage1/apicsrftest5";
let body = JSON.stringify({test: "csrf5"});
let headers = {"Authorization": "Bearer random_value"};

//リクエストを送信
fetch(url, {method, headers, body})

ACAOで許されたオリジンじゃないからだめという旨のエラーが表示されている。(仮にオリジンが許されてもACAHでAuthorizationヘッダも許可しないとエラーになる。てなわけで、動いているシステムでCORS設定ないのにAuthorizationヘッダで認可しているシステムはないと思われる)

6.サーバ側ではリクエストをAuthorizationヘッダ値で認可&ACAO設定有(*)&ACAH設定有

詳細

  • APIを叩くためにAuthorization(値は十分にランダムであるという想定)が必要
  • ACAO:*(すべてのオリジン)
  • ACAH:Authorizationヘッダを許可

結論

Authorizationヘッダにつける値が推測できないという前提なので、CSRFできない。

ただ、別ドメインのJSから参照できる場所にAuthorizationヘッダに付与する値が格納されている場合にはその限りではない。(ローカルストレージに格納してpostMessageでドメイン間やり取りできるようにしている場合とか?そんなことはすべきではないが)


Discussion