APIにおけるCSRFについて【パターン解説】
概要
API(Ajax)
リクエストにおけるCSRF(クロスサイトリクエストフォージェリ)
について、リクエストの認可方法あるいはCORS
設定毎にパターン試行したメモ。
基本事項
- ブラウザはChromeで検証
- POSTメソッド+JSONボディのリクエストを想定する(Content-TypeがJSONとは限らない)
- 別オリジン(www.google.com など)で開発者ツール>consoleでJavaScriptを実行することで、クロスオリジンなリクエストを再現する
- APIはAWS
APIGateway
で作成し、リクエストが到達したかを確認するためにCloudWatch
にログを吐くよう構築 ※APIのドメインはデプロイ時に払いだされたものをそのまま利用
用語の省略
以下のように用語を省略します
この記事での用語の省略
- CSRF:クロスサイトリクエストフォージェリ
- SOP:Same Origin Policy
- 単純リクエスト:プリフライトが飛ばないリクエスト。シンプルリクエストとも呼ばれる。以下記事を参照。
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#単純リクエスト
以下はレスポンスヘッダ
- 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が防がれる。
補足
「サーバが単純リクエストを受け付けてしまう場合」について、主に以下のような例が考えられる
- サーバにはリクエストのカスタムヘッダが存在するか否かという検証がない
- パラメータを下記いずれかの形式で受け付けてしまう
- JSONで受け付けるがContent-Typeヘッダはapplication/jsonでなくてもいい
- Content-Typeヘッダはapplication/x-www-form-urlencodedでパラメータの形式もそれに合わせた形式でいい
- Content-Typeヘッダはmultipart/form-dataでパラメータの形式もそれに合わせた形式でいい
- そもそもパラメータ必要なく、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を検索(以下は参照例)
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