本文は主に 3 つのキーワードに関係しています:
- 同一生成元ポリシー(Same-origin policy、略して SOP)
- クロスサイトリクエストフォージェリ(Cross-site request forgery、略して CSRF)
- クロスオリジンリソースシェアリング(Cross-Origin Resource Sharing、略して CORS)
同一生成元ポリシー SOP#
同一生成元#
まず同一生成元とは何かを説明します:プロトコル、ドメイン名、ポートがすべて同じであることが同一生成元です。
url | 同一生成元 |
---|---|
https://niconico.com | 基準 |
https://niconico.com/spirit | o |
https://sub.niconico.com/spirit | x |
http://niconico.com/spirit | x |
https://niconico.com:8080/spirit | x |
制限#
あなたが クロスオリジンの問題に直面するのは、SOP のさまざまな制限があるからです。しかし具体的に何が制限されているのでしょうか?
もしあなたが SOP を「非同一生成元リソースの取得を制限する」と言うなら、それは正しくありません。最も簡単な例は、画像、CSS、JS ファイルなどのリソースを引用する際にはクロスオリジンが許可されていることです。
もしあなたが SOP を「クロスオリジンリクエストを禁止する」と言うなら、これも正しくありません。本質的に SOP はクロスオリジンリクエストを禁止するのではなく、リクエスト後にリクエストの応答を遮断します。
実際、表面的には SOP には 2 つの状況があります:
- iframe、画像などのさまざまなリソースを正常に引用できますが、その内容に対する操作が制限されます
- ajax リクエストを直接制限します。正確にはajax 応答結果の操作を制限します。これが後で述べる CSRF を引き起こします
しかし、本質的にはこの 2 つは同じです:要するに、非同一生成元のリソースに対して、ブラウザは「直接使用」できますが、プログラマーとユーザーはこれらのデータを操作できず、悪意のある行動を防ぎます。これが現代の安全ブラウザがユーザーを保護する一つの方法です。
以下は、実際のアプリケーションで遭遇する可能性のある 3 つの例です:
- ajax を使用して他のクロスオリジン API にリクエストする、最も一般的な状況で、フロントエンド初心者の悪夢
- iframe と親ページの通信(DOM や変数の取得など)、発生率は比較的低く、解決方法も理解しやすい
- クロスオリジン画像(例えば、
<img>
からのもの)を操作する、canvas で画像を操作する際にこの問題に直面します
もし SOP がなければ:
- iframe 内の機密情報が無制限に読み取られる
- CSRF がさらに無制限に行われる
- インターフェースが第三者に悪用される
クロスオリジンを回避する#
SOP はユーザーをより安全にしますが、同時にプログラマーにとっては一定の程度の面倒を引き起こします。なぜなら、時にはビジネス上の理由でクロスオリジンのニーズがあるからです。クロスオリジンを回避するための方法は、長さの制限があるため、ここでは詳しく説明せず、いくつかのキーワードだけを示します:
ajax に関しては
- JSONP を使用
- バックエンドで CORS 設定を行う
- バックエンドのリバースプロキシ
- WebSocket を使用
iframe に関しては
- location.hash または window.name を使用して情報を交換
- postMessage を使用
クロスサイトリクエストフォージェリ CSRF#
概要#
CSRF(Cross-site request forgery)クロスサイトリクエストフォージェリは、一般的な攻撃手法です。これは、A サイトに正常にログインした後、cookie が正常にログイン情報を保存し、他のサイト B が何らかの方法で A サイトのインターフェースを呼び出して操作を行うことを指します。A のインターフェースはリクエスト時に自動的に cookie を持ち込みます。
上記で述べたように、SOP は html タグを通じてリソースを読み込むことができ、SOP はインターフェースリクエストを阻止するのではなく、リクエスト結果を遮断します。CSRF はまさにこの 2 つの利点を利用しています。
GET リクエストの場合、直接<img>
に放り込むことで、知らず知らずのうちにクロスオリジンインターフェースにリクエストを送信できます。
POST リクエストの場合、多くの例が form を使用して送信します:
<form action="<nowiki>http://bank.com/transfer.do</nowiki>" method="POST">
<input type="hidden" name="acct" value="MARIA" />
<input type="hidden" name="amount" value="100000" />
<input type="submit" value="私の写真を見る" />
</form>
したがって、SOP は CSRF を防ぐ手段としては機能しません。
SOP の制限を振り返ると、これらの 2 つの例は直接 html タグを使用してリクエストを発起しており、ブラウザはこれを許可しています。根本的には、あなたは js で直接取得した結果を操作できないからです。
SOP と ajax#
ajax リクエストに関しては、データを取得した後、あなたは自由に js 操作を行うことができます。この時、同一生成元ポリシーは応答を阻止しますが、リクエストは依然として発信されます。なぜなら、応答を遮断するのはブラウザであり、バックエンドプログラムではないからです。 実際には、あなたのリクエストはすでにサーバーに送信され、結果が返されていますが、安全ポリシーのために、ブラウザはあなたがjs 操作を続けることを許可しないため、あなたがよく知っているblocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
というエラーが表示されます。
したがって、再度強調しますが、同一生成元ポリシーは CSRF を防ぐ手段としては機能しません。
ただし、CSRF を防ぐ例外は存在します。ブラウザはすべてのリクエストを成功させるわけではなく、上記の状況は単純リクエストに限られます。関連する知識は下の CORS のセクションで詳しく説明します。
CSRF 対策#
SOP は CSRF に利用されているため、本当に無力なのでしょうか?
いいえ!SOP が cookie の命名領域を制限していることを覚えていますか?リクエストは自動的に cookie を持ち込みますが、攻撃者はどんな場合でもcookie の内容そのものを直接取得することはできません。
したがって、CSRF に対する考え方はこうです:フロントエンドとバックエンドが分離された一般的な認証方法を使用し、トークン認証を行い、トークンを cookie に保存せず、リクエスト時にリクエストヘッダーに手動で追加します。
もう一つの方法は、cookie の内容をリクエスト時にクエリ、ボディ、またはヘッダーに含めることです。リクエストがサーバーに到達したら、cookie で送信された情報を確認せず、カスタムフィールドだけを見ればよいです。もし正しければ、必ず cookie の本域から送信されたリクエストが見えるはずで、CSRF はこれを実行できません。(この方法はフロントエンドとバックエンドが分離されている場合に使用し、バックエンドレンダリングの場合は直接 DOM に書き込むことができます)
サンプルコードは以下の通りです:
var csrftoken = Cookies.get('csrfToken')
function csrfSafeMethod(method) {
// これらのHTTPメソッドはCSRF保護を必要としません
return /^(GET|HEAD|OPTIONS|TRACE)$/.test(method)
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader('x-csrf-token', csrftoken)
}
},
})
クロスオリジンリソースシェアリング CORS#
クロスオリジンはブラウザの制限であり、クロスオリジンリソースシェアリング(Cross-origin resource sharing)もサーバーとブラウザの調整の結果です。
サーバーが CORS 関連の設定を行った場合、ブラウザへのリクエストヘッダーにはAccess-Control-Allow-Origin
が追加されます。ブラウザはこのフィールドの値が現在の生成元と一致するのを見て、クロスオリジン制限を解除します。
HTTP/1.1 200 OK
Date: Sun, 24 Apr 2016 12:43:39 GMT
Server: Apache
Access-Control-Allow-Origin: http://www.acceptmeplease.com
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: application/xml
Content-Length: 423
CORS に関しては、リクエストは 2 種類に分かれます。
単純リクエスト#
- リクエストメソッドは GET、POST、または HEAD を使用
- Content-Type は application/x-www-form-urlencoded、multipart/form-data、または text/plain に設定
上記の 2 つの条件を満たすものはすべて CORS 単純リクエストです。単純リクエストはすべて直接サーバーに送信され、CSRF を引き起こす可能性があります。
プリフライトリクエスト#
単純リクエストの要件を満たさないリクエストは、最初にプリフライトリクエスト(Preflight Request)を送信する必要があります。ブラウザは本当のリクエストの前に OPTION メソッドのリクエストをサーバーに送信し、現在の生成元が CORS のターゲットに合致するかを確認し、検証が通った後に正式なリクエストを送信します。
例えばapplication/json を使用してパラメータを送信する POST リクエストは非単純リクエストであり、プリフライトで遮断されます。
また、PUT メソッドのリクエストもプリフライトリクエストが送信されます。
上記で述べたCSRF を防ぐ例外は、プリフライトリクエストを指します。クロスオリジンが成功してプリフライトリクエストが送信されても、実際のリクエストは送信されないため、CSRF が成功することはありません。
CORS と cookie#
同一生成元とは異なり、クロスオリジンの CORS リクエストはデフォルトで Cookie や HTTP 認証情報を送信しません。フロントエンドとバックエンドの両方で、リクエスト時に cookie を持ち込むように設定する必要があります。
これが、CORS リクエストを行う際に axios がwithCredentials: true
を設定する必要がある理由です。
以下は node.js のバックエンド koa フレームワークの CORS 設定です:
/**
* CORSミドルウェア
*
* @param {Object} [options]
* - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, デフォルトはリクエストのOriginヘッダー
* - {String|Array} allowMethods `Access-Control-Allow-Methods`, デフォルトは 'GET,HEAD,PUT,POST,DELETE,PATCH'
* - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
* - {String|Array} allowHeaders `Access-Control-Allow-Headers`
* - {String|Number} maxAge `Access-Control-Max-Age`(秒単位)
* - {Boolean} credentials `Access-Control-Allow-Credentials`
* - {Boolean} keepHeadersOnError エラーが発生した場合に`err.header`にヘッダーを追加
* @return {Function} corsミドルウェア
* @api public
*/
ちなみに、Access-Control-Allow-Credentials
をtrue
に設定すると、Access-Control-Allow-Origin
は強制的に*
に設定できなくなります。安全のため、これはかなり面倒です...
開坑予告#
とりあえずここまでにします。質問があればコメント欄に提出してください。今後は XSS、CSP、http/https に関連するテーマについても話す予定です。
- 2020-02-13 更新 フロントエンドネットワークセキュリティ必修 2 XSS と CSP
- 2020-06-14 本文更新、一部の説明をより正確にしました
原文へのリンク:https://ssshooter.com/2019-11-08-csrf-n-cors/