参考:https://qiita.com/mpyw/items/0595f07736cfa5b1f50c
参考:https://qiita.com/netebakari/items/41baa7e1d0b8d89f9d12

久々すべてを自前でアプリを作ったので、復習をしてみた。

概要

サイトを跨ぐ偽造リクエストの送信である。
具体的には罠サイトに掲載されているリンクやフォーム送信を行うと、攻撃対象のサイトに副作用を目的としたリクエストが勝手に送られるというものである。

基本的なサイト方針としては、許可するページ以外でのリクエストを失敗するようにすること。

対策方法1:Nonceトークンを使用する

一時的に利用するNonceトークンを埋め込み対策を行う。
トークンには固定トークン(ログイン完了後ずっと固定値)、あるいはワンタイムトークン(一回の送信ごとに変更)、またWordPressの場合は一定時間ごとに変更されるトークンがある。

攻撃されたページにトークンを記載しても一定期間でトークンが変更されるため、意味がない。
罠が仕掛けられたサイトの場合は、基本的にはフロントエンドプログラムしか設置されないため、同一オリジンポリシーにより攻撃対象サイトのNonceは取得できないため、安全性が高く容易に実装できる。

対策方法2:固有のHTTPヘッダーの検証

通常のフォーム送信には付与されない固有のHTTPヘッダーをフロントエンド側で付与し、サーバーサイドでそれが送られているか検証する方法であるため、WebAPIのみで使用される。
Nonceとは違いステートレスになるため、WebAPIでよく使用される。

下記のようなヘッダがよく使用される。

# 古くから使われているヘッダー
X-Requested-With: XMLHttpRequest
# JWT認証で使用されるヘッダ
Authorization: Bearer XXXXX

以下実装例。

// フロントエンド側JS
const response = await fetch('/posts', {
  method: 'POST',
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: document.getElementById('name').value,
    body: document.getElementById('body').value,
  }),
});
const json = await response.json();

console.log(json);
// バックエンド側PHP
<?php 

if (!isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
    header('Content-Type: application/json', true, 400);
    exit('{"error":"invalid request"}');
}

固有のHTTPヘッダーで対策できる理由としては、HTMLフォームではHTTPヘッダーの付与が許可されていないこと
またJavaScript の fetch() や XMLHTTPRequestではHTTPヘッダーは自由に付与できるが、クロスオリジン のリクエスト先に固有のHTTPヘッダを付与しようとするとプリフライトリクエスト(リクエスト前の事前チェック)にブロックされるためである。

対策3:Originヘッダーの検証

サーバーサイドの実装のみで対策でき、非常に低コストに実装できる方法である。

Originヘッダーは以下のような形でWebブラウザから送られてくる。
この値は現在のオリジンを示しており、Chromeエクステンションなどを使用しない限りは偽装することは不可能であるため、意図しないオリジンからのリクエストをはじくことができる。

Origin: https://example.com

ブラウザによっては同一オリジンの場合は、Originヘッダを付与しないという動きをする場合があるため、Origin ヘッダが送信されてきていて、かつ許可しないオリジンであれば無効化という処理をサーバサイドに書く必要がある。
また、GET リクエストの場合にはクロスオリジンでも付与されない点に注意が必要。

以下実装例。

<?php

if (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['HTTP_ORIGIN'] !== 'https://example.com') {
    exit;
}

ちなみにReferer ヘッダーは送らないように設定するユーザーがいることもあり、またReferrer Policyを用いることでRefererを送らないように設定できるため、Refererがないとリクエストを弾くという実装はこのましくない。

対策4:Same Site Cookie

ログイン機能があるサイト限定ではあるが、Same Site Cookie でも対策をとることが可能である。
以下、Set-Cookieヘッダの例。

Set-Cookie: __Host-SID=q1w2e3r4t5; HttpOnly; Secure; Path=/; SameSite=Lax;

Strict という値があるが、この場合はSameSite以外のすべてのリクエストで一切Cookieを送らなくなる。
そのため、別のサイトからリンクで遷移した場合にもCookieが送られなくなるため、利便性の面で適用が難しい。
Laxという値は、Top Level Navigation(ブラウザのアドレスバーに表示されるような画面遷移)以外はCookieを送らないという仕様であるため、こちらを使用するのが望ましい。
Top Level Navigationにはブラウザのポップアップウインドウや<link rel=”prerender” />も含まれるため、いくつかの攻撃手段には配慮する必要がある。

ちなみにChromeではすでにSameSite=Laxがデフォルトになっている。

参考:https://blog.jxck.io/entries/2020-02-25/end-of-idyllic-cookie.html
参考:https://blog.jxck.io/entries/2018-10-26/same-site-cookie.html


以上に関しては、CSRFを防ぐために同一オリジンのみを許可する設定だったが、逆に別オリジンの送信を許可する設定(CORS Cross-Origin Resource Sharing)についても解説してみる。

Access-Control-Allow-Orign ヘッダー

これをレスポンスヘッダーに含めると、指定したオリジンからの読み取りを許可することができる。

# すべてのオリジンからのアクセスを許可する場合
Access-Control-Allow-Origin: *
# 特定のオリジンからのアクセスを許可する場合
Access-Control-Allow-Origin: https://example.com

ただし、この設定はリクエスト自体は通ってしまい、レスポンスも返ってきてしまう
単純にJavaScriptからはその値がわからないだけであることに注意する。

つまり、副作用が起こるようなサイトでJavaScriptでPOST送信を行うと、実際にサイトが操作されてしまうということだ。

つまりPOST送信の場合は、サーバー側で別オリジンのPOST送信を防ぐようにしないと意味がない。
そのためにはAccess-Control-Allow-Originヘッダー対策以外に、上述のプリフライトリクエストの検証を行うと手っ取り早くて良い。

プリフライトリクエストは別ドメインへのリクエストで通常のフォーム送信ではつかないようなリクエストヘッダーがついていた場合、 OPTIONS メソッドリクエストが先に送信される。
ブラウザはOPTIONリクエストのレスポンスで Access-Control-Allow-Origin ヘッダ等を検証し、ここで検証が失敗すればそれ以降のリクエストはキャンセルする。
問題がなければ改めて本のリクエストを送信する(つまり1回のXHRが2回のリクエストを発生させることになり、オーバーヘッドが生じるため、セキュリティを必要とするページ以外にはつけない)。

つまり、適当なヘッダーをつけてリクエスト送信を行うような仕様にし、ヘッダーのチェックをサーバー側で、またAccess-Control-Allow-Originヘッダーの指定を行うことで対策となる

まとめとして、プリフライトレスポンスの Access-Control-Allow-Origin は,次のリクエストが実行可能かどうかを左右する。
メインレスポンスの Access-Control-Allow-Origin は、結果をフロントエンドで読み取れるかどうかだけに関わり、サーバサイドでアクションが実行されるかどうかには影響しない。

JSONP

現在は推奨されていない方法である。
script要素での読み込みはクロスオリジン でも利用できるという抜け道を使用する。

<script src="https://example.com/api.js?callback=func"></script>

この方法はGETメソッドしか使えず、サーバー側(別オリジン)でクライアントサイドで定義されている任意の関数を実行できるという問題があるため、あまり使用されていない。

/* https://example.com/api.js */

var url = new URL(window.location.href);
var params = url.searchParams;
setTimeout(() => {
  window[params.get('callback')]();
}, 1000);

// ほかにもいろいろできちゃう

まとめ

いつもNonceばかり使っていたが、いずれも基本的なことではあるが、一から大規模になってくると堅守性を維持すると複雑になりがちなので、フレームワークに頼った方が安全説が身に染みて分かった。
また当たり前だが、サーバー側ではGETメソッドでリクエストされるページには副作用をなるべく持たせないことも忘れないように。
※足跡や既読機能がある場合は難しそうだが