こんにちは、たかぴろです
最近はおうちハニーポットを作って LT をしました。セキュリティっぽい内容なので興味あったら見てみてください。
おうちハニーポットを作りましたhttps://t.co/iO4G0Haznx
— たかぴろ (@takapiro_99) 2021年4月24日
本題
Flask
+ React
(Vue
) での SPA Web アプリ、最近のハッカソンでよく見る構成ですよね。
でも毎回こいつが出てきます
おれ「またおまえか」
おれ「どれどれ… no-cors
を指定すればいいんだな?」
レスポンス「opaque なのでレスポンスを見せられません(意訳)」
おれ「なにもわからん」
CORS ってなんなのか
CORS は Cross-Origin Resource Sharing の略で、クロスなオリジンのリソースのシェアリングです。
オリジンは「プロトコル + ドメイン + ポート番号」で、ページの配信元 と リクエストを送りたいところ の間でどれか一つでも違うとき、オリジンが異なる(クロスオリジン)ということになります。
Django や RoR などのフルスタック Web フレームワークは大体オリジンが同じですが、Flask を Heroku に + React を Firebase に みたいな、API サーバーとフロントエンドが分かれているような場合はクロスオリジンということになります。(CORS が必要)
オリジンが異なる ≒ 他人からのリクエスト なので、悪意のあるリクエストが飛んでくる可能性があり(ホンマか?)、それを回避するために、あらかじめサーバーで CORS (クロスオリジン間リソース共有)を許すオリジンを指定しておける というものらしいです。
Same-Origin 又は Cross-Origin("おれ"は許してくれるとき)の場合
おれ → リクエスト送るね
レスポンスだよ ← サーバー
Cross-Origin(CORS しないよ)の場合
Aさん → リクエスト送るね
知らない子ですね(レスポンスなし) ← サーバー
なぜ CORS を制限するのか
セキュリティ対策なのはそれはそうなのですが、主に XSS と CSRF の2つを対策しています。(以下 Qiita 引用 *1)
XSS
CSRF
CORS 設定はブラウザ(の XHR や fetch )だけで有効 ← 重要
対策を行いたい XSS や CSRF(しーさーふ)の攻撃は、全て「ブラウザ」と「その奥にいる人」がいて成り立つものです。
それ以外の curl
でのリクエストやサーバー間通信、もしくは Python
などのプログラムから通信する場合、どのオリジンからも普通にリクエストを送れます。(Same-Origin Policy がない)
不正リクエスト対策とは別に、意図していない リクエストを防げるという仕組みなのですね。勉強になりました。
個人的にはこの段落が一番の読みどころです。
CORS を許していくには
さて、具体的には、(サーバー)応答時にレスポンスヘッダーの中の Access-Control-Allow-Origin
ヘッダーに、誰に CORS を許すのかを記述します。
例えば僕がサーバーを開発したとして、 example.com
からのリクエストしか受け付けないぜ、としたい場合、レスポンスに
Access-Control-Allow-Origin: https://example.com
を指定します。これがあればちゃんと通信ができ、なければできません。的な感じです。
ワイルドカード(Access-Control-Allow-Origin: *
)も使えますが、credential mode のときは無効にされるらしいです。
*2 MDN の CORS のドキュメント
Flask で CORS 設定
ググったら色々出てきますが、ブログ記事などはちょっと古い記事が多い印象です。
flask_cors
というパッケージがあり、これを使って
from flask_cors import CORS app = Flask(__name__) CORS(app) # これで CORS 対応!
みたいに超簡単にできます。(???)
簡単ですよね
さすがにこれだと 全てのオリジンを許可 という状態になってしまっているので
from flask_cors import CORS app = Flask(__name__) CORS(app, origins=["https://example.com", "http://localhost:3000"]) # これで CORS 対応!
のように、ある程度絞った状態で CORS を許可するのが良いのではないでしょうか。(ベストプラクティスは分かりませんが)
*3 flask_cors 公式ドキュメント
でもこのライブラリ、前使ったときにうまく動かなくて、試行錯誤の結果 Flask のデコレータの機能で手動でヘッダーを設定してみる、というのでも動いたのでそれも載せておきたいと思います。
リクエストのパスごとに細かくヘッダー制御することも可能です。
*4 多分そのとき動かなかった理由
app = Flask(__name__) # すべてのリクエストに対してヘッダーをつけていく @app.after_request def after_request(response): response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "*") response.headers.add("Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS") return response # 以下 @app.route('/')... などなど
動けばヨシ!みたいな精神なので、こんな感じで失礼します…
Heroku × Flask で CORS 設定
最近 Flask × Heroku で開発していた時に起きた怪奇現象を紹介します。
おれ「よっしゃこれで CORS 設定できたかな」
おれ「フロントエンドからリクエストを送ってみよう」
ブラウザ「ちゃんと通信できたよ!」
~数秒後~
ブラウザ「」
おれ「は?」
原因
まず、Heroku にアップロードしたアプリは様々な理由でクラッシュします。
再実行を試み、復活するまでは Heroku が代わりのページを表示してくれています。
が、このページでは CORS が無効なので、「リクエストは送れるけど、なぜかさっきまでできてた CORS ができなくなってる」という状況になります。
今回は、リクエストがいくつかまとまってきたら落ちる というサーバー側の実装の都合でアプリが頻繁に落ちていて、フロントエンドから見ると不思議なことになっていました。
友達と夜遅くにペアプロしてたのですが、本当に何もしてないのに壊れているような感じがして怖かったです。
感想
「CORS 対策」とか「CORS で動かない」とか言いますが、意味的には逆なんだなって思いました。
教訓
- ログをちゃんと見る
- エラー文とかドキュメントとかちゃんと読む
おわり!