💬

SOPとCORSの基本とデバッグ

2024/12/28に公開

2024年も残すところ僅かとなりました。たくさんの方々にお世話になりました。感謝です。総じて振り返ってみると、自分の課題である「内省力」を磨けた1年だったかなと思います。来年もよろしくお願いいたします。

はじめに

Webアプリケーションの開発において、セキュリティと利便性のバランスを取ることは非常に重要です。特に、同一オリジンポリシー(Same-Origin Policy, SOP)クロスオリジンリソース共有(Cross-Origin Resource Sharing, CORS) は、Webブラウザとサーバ間のリソース共有を制御する上で欠かせない概念です。しかし、これらのメカニズムは一見複雑で、正しく理解・設定しないと、思わぬエラーやセキュリティ上のリスクにつながる可能性があります。

本記事では、Pythonの高速なWebフレームワークである FastAPI を用いて、SOPとCORSの基礎を解説し、実際にどのように設定・検証するかを詳しく説明します。また、Postman を使ってリクエストのヘッダーやレスポンスを確認し、CORS設定の有無による挙動の違いを具体的に示します。

本記事は以下のスコープを対象とします。

  • 同一オリジンポリシー(SOP)の仕組みとその重要性を理解する
  • クロスオリジンリソース共有(CORS)の基本概念と設定方法を学ぶ
  • FastAPIにおけるCORSの具体的な設定手順を把握する
  • Postmanを用いてCORSの挙動を検証し、問題の原因を特定する

同一オリジンポリシーの基礎

同一オリジンポリシー(Same-Origin Policy, SOP) とは、ブラウザが実装しているセキュリティモデルの一部で、あるオリジンでロードされたドキュメントやスクリプトが、別のオリジンからのリソースとどのように相互作用できるかを制限するものです。

このオリジンという概念は、プロトコル(httpまたはhttps)、ホスト(ドメイン)、およびポート番号によって定義されます。例えば、https://www.kaza.oooは、プロトコルがhttps、ホストがwww.kaza.ooo、ポートが443のオリジンとして定義されます。なお、httpsプロトコルの場合、ポート番号は暗黙的に443番がアサインされています。

このSOPによって、同一オリジンへのリクエストは自由に許可されます。https://www.kaza.oooで実行されているスクリプトは、同じオリジンのhttps://www.kaza.ooo/api/dataへのリクエストは問題なく許可されデータを取得することができます。一方で、異なるオリジン(例えば、http://example.org/api/data)に対してリクエストを行う場合にはSOPが適用され、ブラウザからのリソースへのアクセスがブロックされ、データを取得することができません。ただ、画像の取得・表示はその限りではありませんが、ピクセルデータにアクセスできないという意味ではSOPの制限が適用されていることになります。

これによって何が嬉しいのかというと、もし、異なるオリジンが悪意のあるWebサイトだった場合に、Cookieなどの認証情報が不正に利用されるリスクを回避することができる点にあります。JavaScriptなどのスクリプトとが異なるオリジン間で自由にアクセスできると、Webアプリケーションの脆弱性を利用した攻撃の被害に遭う可能性があるからです。ただ、この制約によって、信頼できるオリジンへのアクセスも制限されてしまう点はあまり嬉しくは無い点です。可用性と安全性のトレードオフなのかもしれません。ただ、これを解決するのがCORSです。

CORSの基本概念

CORS(Cross-Origin Resource Sharing:クロスオリジンリソース共有) は、SOPの制限を緩和するためのメカニズムです。これは、サーバ側で特定のオリジンを設定されすれば、異なるオリジン間でもリクエストを許可し、データを共有することを可能とします。

CORSの有無による動作の比較

具体的には、Access-Control-Allow-Originヘッダをレスポンスに追加することで、特定のオリジンからのリクエストを許可できるようになります。PythonとFastAPIで設定する場合は以下のようになります。

app = FastAPI()

origins = [
  "http://localhost:8080",
  "https://www.kaza.ooo",
]

app.add_middleware(
  CORSMiddleware,
  allow_origins=origins,
  allow_credentials=True,
  allow_methods=["*"],
  allow_headers=["*"],
)

これをPostmanで検証し、リクエストヘッダとレスポンスヘッダが実際にどうなっているのかを確認してみます。予めallow_originsに設定していたオリジンhttp://localhost:8080がリクエストヘッダに含まれている場合、Access-Control-Allow-Originヘッダがレスポンスに追加されている様子が確認できます。
CORS-Succusessed

一方で、allow_originsに設定していないオリジンhttp://localhost:8081がリクエストヘッダに含まれている場合にはAccess-Control-Allow-Originヘッダがレスポンスに追加されていません。ウェブブラウザ経由でリクエストされていた場合はCORSに関するエラーが出力される状態です。
CORS-Failed

どちらの場合でも200番のレスポンスコードが返ってきていますが、これはPostmanがブラウザではないからです。デスクトップアプリのPostmanはそのOS上で動いており、WebアプリのPostmanは、おそらくPostmanクラウドサーバー経由でリクエストを送信するため、CORS制約を受けない状態だと推察されます。OS上で動いているモバイルアプリなんかも同様のロジックでCORS制約を受けることはありません。あくまで、実際のブラウザから直接エンドポイントにアクセスされた場合はコンソールにCORSに関連するエラーが出力されます。

CORSに関するHTTPヘッダーの詳細

前述したAccess-Control-Allow-Originヘッダ以外にもCORS関する様々なヘッダを設定することができます。その中でも、Access-Control-Allow-CredentialsはCookieやHTTP認証、SSL証明書などの資格情報を扱う際に非常に重要な項目です。この資格情報を異なるオリジンに対して許可するか否かを管理します。
資格情報を異なるオリジンに渡せることで何が嬉しいのかというと、例えば、異なるオリジン間でもユーザはログイン状態を維持したままデータをリクエストすることができるようになるあたりですね。これはUXを向上させることに繋がります。また、異なるマイクロサービスが複数のドメインやサブドメインで動作しているマイクロサービスアーキテクチャや、同じSaaSプラットフォーム内で複数のモジュールやサービスが存在し、それぞれが異なるサブドメインやドメインで動作しているコンパウンドSaaSみたいな領域では強力な効果を発揮するんじゃないかと思います。

まとめ

マイクロサービスアーキテクチャとコンパウンドSaaSって同じなのかな?ここは今後しっかり理解していきたい部分ですね。

参考

Discussion