Cosnomi
Cosnomi

医療×IT / 医学生 / Web(React, Flask) / 機械学習(画像認識, Keras)

Twitter / GitHub / Keybase

同一生成元ポリシー(SOP)を正しく回避 CORS(オリジン間リソース共有)

以前、 SOP(同一生成元ポリシー)について説明しました。この記事では、その内容の理解を前提として話を勧めます。

軽く復習します。SOP による制限は厳しいもので、api.example.com と example.com さえ異なるものとして制限を受けます。これはアクセス元のスクリプトが悪意を持ってクライアントにアクセス先へアクセスさせるのを防ぐためで、ブラウザとしてはアクセス先がアクセス元を信頼しているということさえ分かれば制限する必要はないわけです。その信頼関係を伝える手段がオリジン管理リソース共有(Cross-Origin Resource Sharing: CORS)です。

概要

いくら SOP による制限が不便だからといって、いかなる場合でもクロスオリジンのアクセスを認めるなんてことはできません。アクセス元オリジンとアクセス先オリジンに信頼関係があるということが前提です。そして、その信頼関係はアクセス先から確認する必要があります。

信頼関係を確認する仕組み

悪意のない Web サービスが SOP によって不利益を被る最たる例は XMLHttpRequest だと思われますから、これを例にとって説明したいと思います。

単純リクエスト

簡単なリクエストの場合(メソッドが GET/HEAD/POST のいずれか, 限定されたヘッダー、限定された Content-Type)は、単純リクエストが行われます。

ブラウザは HTTP リクエストヘッダにアクセス元 Origin の情報をOrigin ヘッダーに含めます。なお、オリジンは正規化されて送られます。(例: Origin: http://example.com)

アクセスされたサーバーから

Access-Control-Allow-Origin: http://example.com」のように応答を受け取ることで、アクセス先オリジンとhttp://example.comオリジンは信頼関係にあるとブラウザは判断します。

preflight リクエスト

上記に当てはまらないリクエスト(PUT メソッドの場合や Content-Type が application/json の場合など)は実際のリクエストを行う前に preflight リクエストを行います。これは、単純なリクエスト以外のリクエストをいきなり送りつけるのはユーザーデータに影響を及ぼす可能性があるからです。

preflight リクエストは OPTIONS メソッドを利用して主に以下のヘッダーを送信します

  • Origin
  • Access-Control-Request-Method (本番の要求で使われるメソッド)
  • Access-Control-Request-Headers (本番の要求で含まれるカスタムヘッダー)

これに基づいて、サーバーはこの要求を受け入れるかどうかを応答します。これには以下が含まれます。

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods (許可されるメソッド)
  • Access-Control-Allow-Headers (許可されるヘッダー)
  • Access-Control-Max-Age (preflight 要求をキャッシュ可能な秒数)

ブラウザは自分の要求が受け入れられている(つまり、Allow-Methods や Allow-Headers に Request-Methods や Request-Headers が含まれる)ことを確認したら単純リクエストと同じように実際のリクエストを行います。

資格情報を含む要求

通常、クロスオリジンのアクセスではブラウザは資格情報(credentials)を送信しません。しかし、明示的な指定があった場合、ブラウザは資格情報を利用したクロスオリジンのアクセスを試みます。

サーバーが単純リクエストに対する応答または preflight 要求に対する応答に「Access-Control-Allow-Credentials: true」が含まれていればブラウザは資格情報を含むリクエストによって得られたそのデータの利用を認めます。一方、true でなければ、その応答を破棄し、Web サイトでのそのデータの利用を認めません。

nginx で Access-Control-Allow-Origin の設定

規定されているヘッダーを含めるようにすれば良いわけです。

単純要求だけしか来ないなら一例として…

    location / {
       add_header Access-Control-Allow-Origin '*' always;
       add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
       add_header Access-Control-Allow-Headers "Origin, Content-Type, Authorization";
       add_header Access-Control-Allow-Credentials true;
    }

もし、preflight リクエストも考慮するなら

    location / {
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin '*' always;
            add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
            add_header Access-Control-Allow-Headers "Origin, Content-Type, Authorization";
            add_header Access-Control-Allow-Credentials true;
            return 204;
         }
       add_header Access-Control-Allow-Origin '*' always;
       add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
       add_header Access-Control-Allow-Headers "Origin, Content-Type, Authorization";
       add_header Access-Control-Allow-Credentials true;
    }

まとめ

  • SOP を信頼できるオリジン間に限定して解除するために CORS という方法がある
  • CORS では、アクセス先サーバーから Access-Control-Allow-Origin ヘッダを受け取る必要がある
  • 複雑なリクエストでは preflight リクエストを行う必要がある
  • サーバー側の設定は nginx の config だけでよい

コメントフォームは設置していませんので、ご意見・ご感想などはTwitter(@cosnomi)などへお願いします。