たかぴろのブログ

雑多なアウトプット

Flask と Heroku と CORS

こんにちは、たかぴろです

最近はおうちハニーポットを作って LT をしました。セキュリティっぽい内容なので興味あったら見てみてください。


本題

Flask + React(Vue) での SPA Web アプリ、最近のハッカソンでよく見る構成ですよね。

でも毎回こいつが出てきます

Access-Control-Allow-Origin がないよっていうエラー
Access-Control-Allow-Origin がないよっていうエラー


おれ「またおまえか」

おれ「どれどれ… no-cors を指定すればいいんだな?」

レスポンス「opaque なのでレスポンスを見せられません(意訳)」

おれ「なにもわからん」


CORS ってなんなのか

CORS は Cross-Origin Resource Sharing の略で、クロスなオリジンのリソースのシェアリングです。

オリジンは「プロトコル + ドメイン + ポート番号」で、ページの配信元リクエストを送りたいところ の間でどれか一つでも違うとき、オリジンが異なる(クロスオリジン)ということになります。

DjangoRoR などのフルスタック Web フレームワークは大体オリジンが同じですが、Flask を Heroku に + React を Firebase に みたいな、API サーバーとフロントエンドが分かれているような場合はクロスオリジンということになります。(CORS が必要)

オリジンが異なる ≒ 他人からのリクエスト なので、悪意のあるリクエストが飛んでくる可能性があり(ホンマか?)、それを回避するために、あらかじめサーバーで CORS (クロスオリジン間リソース共有)を許すオリジンを指定しておける というものらしいです。



Same-Origin 又は Cross-Origin("おれ"は許してくれるとき)の場合

おれ → リクエスト送るね
      レスポンスだよ ← サーバー

Cross-Origin(CORS しないよ)の場合

Aさん → リクエスト送るね
      知らない子ですね(レスポンスなし) ← サーバー



なぜ CORS を制限するのか

セキュリティ対策なのはそれはそうなのですが、主に XSS CSRF の2つを対策しています。(以下 Qiita 引用 *1

XSS

  • 不正なスクリプトが実行されるのはユーザーの Web ブラウザ
  • 受ける被害は機密情報を抜かれる、偽情報が表示され社会的信用を落とされるなど
  • 対策として HTML の特殊文字エスケープが有効

CSRF

  • 不正なスクリプトが実行されるのは Web サーバ
  • 受ける被害は通販サイトであれば商品の不正な購入、掲示板サービスだったら不正な書き込みなど
  • 対策として、副作用が発生する画面の実行時に、想定通りの画面遷移が行われたかどうかを確認する。または、事前に渡したトークンが正しいものであるかをチェックするということが有効


CORS 設定はブラウザ(の XHR や fetch )だけで有効 ← 重要

対策を行いたい XSSCSRF(しーさーふ)の攻撃は、全て「ブラウザ」と「その奥にいる人」がいて成り立つものです。

それ以外の 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 設定できたかな」

おれ「フロントエンドからリクエストを送ってみよう」

ブラウザ「ちゃんと通信できたよ!」


~数秒後~


ブラウザ「Access-Control-Allow-Origin がないよっていうエラー

おれ「は?」


原因

まず、Heroku にアップロードしたアプリは様々な理由でクラッシュします。

f:id:sum6:20210425195301p:plain
サーバーログの一部

再実行を試み、復活するまでは Heroku が代わりのページを表示してくれています。

f:id:sum6:20210425195207p:plain
アプリがクラッシュしたときに出してくれる画面

が、このページでは CORS が無効なので、「リクエストは送れるけど、なぜかさっきまでできてた CORS ができなくなってる」という状況になります。


今回は、リクエストがいくつかまとまってきたら落ちる というサーバー側の実装の都合でアプリが頻繁に落ちていて、フロントエンドから見ると不思議なことになっていました。


友達と夜遅くにペアプロしてたのですが、本当に何もしてないのに壊れているような感じがして怖かったです。


感想

「CORS 対策」とか「CORS で動かない」とか言いますが、意味的には逆なんだなって思いました。


教訓

  • ログをちゃんと見る
  • エラー文とかドキュメントとかちゃんと読む


おわり!