Dev Study
← Webセキュリティの実務
レッスン 3 / 12·約 9 分·第19期 Webセキュリティの実務

プリフライトの線は、フォームが送れた範囲に引かれている

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回は、CORS が止めるのは読むことだと見ました。今回は、ブラウザが要求を送る前に何を確かめ、何を確かめないかを見ます。

まず結論

別オリジンへの要求のうち、事前確認(プリフライト)が付くのは一部だけです。Fetch Standard はその線を「HTML のフォーム要素でできる範囲より込み入った要求」と書いており、事前確認なしで送られるメソッドと Content-Type は、フォームが送れる範囲とほぼ重なります。

このコースの芯は「ブラウザが確かめられるのは『どこから来たか』まで、『本人が望んだか』はサーバが自分で確かめるしかない」です。フォームの範囲に収まる要求は、確認されずにサーバまで届きます。

? 読む前に予測

a.test のページのスクリプトから、b.test の同じ宛先へ POST を4回送ります。変えるのは Content-Type だけ(urlencoded / multipart/form-data / text/plain / application/json)。b.test は CORS 用のヘッダを一切返しません。b.test のログに残るものとして正しいのはどれでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

線はどこに引かれているか

別オリジンへ要求を送るとき、ブラウザは要求によって、本体の前に OPTIONS を送って宛先の許可を確かめます。これがプリフライトです。すべての要求に付くわけではありません。線の位置を、Fetch Standard §3.3.1 はこう書きます。

For requests that are more involved than what is possible with HTML’s form element, a CORS-preflight request is performed, to ensure request’s current URL supports the CORS protocol.

基準は HTML のフォーム要素でできること です。具体的な条件は2つの定義に分かれています。

A CORS-safelisted method is a method that is GET, HEAD, or POST.

Content-Type については、essence が次の3種でなければ safelisted ではない、と書かれます。

If mimeType’s essence is not "application/x-www-form-urlencoded", "multipart/form-data", or "text/plain", then return false.

§4.1 の主な条件はこの2つの組み合わせで、メソッドが safelisted でないか、safelisted でないヘッダ名が1つでもあればプリフライトを行う、と定めます。値の長さにも上限があり、1つのヘッダ値が128バイトを超えるか、safelisted なヘッダ値の合計が1024バイトを超えると、事前確認の対象になります。

プリフライトの有無は、要求の中身で決まる
別オリジンへの要求スクリプトの fetch など
メソッドとヘッダを判定
GET / HEAD / POST かつ safelisted なヘッダだけContent-Type はフォームの3種
それ以外PUT、application/json、独自ヘッダなど
送り方
本体をそのまま送る確認なしでサーバに届く
先に OPTIONS を送る許可が返らなければ本体は送らない
  • ・左の列は、スクリプトが応答を読めなくても、要求そのものはサーバに届く
  • ・Fetch §3.3.1 は線の基準を HTML’s form element と書いている
具体実物

4つの Content-Type を送ってみる

自前の最小サーバを 127.0.0.1 だけで待ち受けさせ、CONNECT プロキシで a.test と b.test の2つの名前をそこへ流しました(/etc/hosts は書き換えていません)。そのうえで a.test のページのスクリプトから b.test へ POST を4回送りました。変えたのは Content-Type だけです。b.test は OPTIONS に 204 を返しますが、CORS 用のヘッダは一切返しません。

ブラウザは Playwright 1.63.0 に同梱された Chromium 153.0.8010.12・Firefox 155.0・WebKit 26.6 で、市販のブラウザそのものではありません(macOS 26.5.2 上、各エンジン2回)。

b.test のログ(Chromium 1回目。4項目を抜き出して整形)コマンド出力
method content-type access-control-request-*
POST application/x-www-form-urlencoded -
POST multipart/form-data; boundary=x -
POST text/plain -
OPTIONS - method=POST, headers=content-type

3エンジン×2回の6回すべてで同じ並びでした。最後の行だけが OPTIONS で、acr-method: POST と acr-headers: content-type を持っています。application/json の本体の POST は、ログに現れていません。

スクリプト側は4回とも TypeError でした(Chromium は Failed to fetch、Firefox は NetworkError when attempting to fetch resource.、WebKit は Load failed)。スクリプトから見た結果は同じでも、サーバに届いたものは違います。 3種は本体がサーバに届いており、読めなかったのは応答だけです。

✓ ここまでの確認

実測では、スクリプト側は4回とも同じ TypeError でした。サーバ側の違いの説明として正しいのはどれですか。

抽象原理

フォームが送れる範囲と重ねる

HTML Standard のフォーム送信の節を並べると、線の位置が見えます。enctype 属性のキーワードは3つです。

  • The "application/x-www-form-urlencoded" keyword and corresponding state.
  • The "multipart/form-data" keyword and corresponding state.
  • The "text/plain" keyword and corresponding state.

method 属性は get・post・dialog で、dialog は送信しません。

Fetch の safelisted と HTML のフォーム(各 Living Standard、2026-09-24 取得)表
Fetch(事前確認なし) HTML フォーム
メソッド GET / HEAD / POST get / post
Content-Type urlencoded / multipart/form-data / text/plain 同じ3種(既定は urlencoded)

違うのはメソッドの行の HEAD だけで、フォームからは送れないのに safelisted に入っています。その理由はこの教材では確かめていません。

ここからは教材側の読みです。フォームの範囲に収まる要求は、スクリプトが無くても別サイトのページから送れました。そこにプリフライトを足しても、送られてしまう要求は減りません。事前確認が意味を持つのは、フォームでは作れなかった要求のほうです。

裏返すと、プリフライトは「本人が望んだか」を確かめる仕組みではありません。 確かめるのは宛先が CORS に対応しているかで、フォームの範囲の要求はその確認すら経ずに届きます。

具体実物

同じ形の線を、プロセス分離にも見る

『ブラウザの仕組み』コースの「分離の単位は origin ではなく site」で、Chromium が site 単位でプロセスを分ける理由を読みました。

We use sites instead of origins to avoid breaking compatibility with existing web pages that might modify their document.domain to communicate across multiple subdomains of a site.

document.domain で同じサイト内の別オリジン同士が触れ合えるページが、既に存在していました。オリジンで分けると、それらが動かなくなります。そこで線は site に引かれました。同じ文書は Limitations の節で結果も書いています。

This allows multiple origins within a site to share the same process.

同じ site の別オリジン同士は、既定では同じプロセスを共有しうる。 同じ節には currently とあり、オリジンごとに専用のプロセスを割り当てる実験的なモード(chrome://flags#strict-origin-isolation)にも同じページが触れています。

プリフライトと同じ形です。新しい仕組みの境界が、既存の Web で既にできていたことの外側に引かれ、内側はその仕組みの守備範囲から外れています。

✓ ここまでの確認

「プリフライトがあるので、別サイトのページからの状態変更の POST は、サーバが許可しない限り届かない」という理解は妥当ですか。

⇄ 2つを見比べる
具体例 A

プリフライト(Fetch Standard)

事前確認が付くのはフォームでできる範囲より込み入った要求だけです。GET・POST とフォームの3種の Content-Type は、確認なしでサーバに届きます。

線の内と外表
範囲 プリフライト
フォームで送れる要求 なし(そのまま届く)
それより込み入った要求 あり(許可が無ければ本体を送らない)
具体例 B

プロセス分離の単位(Chromium の Site Isolation)

分離の単位はオリジンではなく site です。document.domain で触れ合う既存ページを壊さないためと Chromium の文書が書いています。同じ site の別オリジン同士は、既定の分離では隔てられるとは限りません。

線の内と外表
範囲 プロセス分離
別の site 同士 あり
同じ site の別オリジン同士 既定では同じプロセスを共有しうる

要求の事前確認と、プロセスの分け方。層はまったく違いますが、境界の引き方に共通するものは何でしょうか。

それぞれの境界の内側に、何が残っているかを見てください。

この章のまとめ

  • プリフライトの基準を、Fetch §3.3.1 は HTML’s form element でできる範囲より込み入った要求 と書く。
  • safelisted method は GET・HEAD・POST、Content-Type は urlencoded・multipart/form-data・text/plain の3種。値は1つ128バイト、合計1024バイトまで。
  • HTML フォームは get / post と同じ3種の enctype。差はフォームから送れない HEAD だけ(理由は未確認)。
  • 実測(Playwright 同梱の3エンジン×2回)では、3種の POST はそのまま届き、application/json だけ OPTIONS で止まった。スクリプト側のエラーは4回とも同じ。
  • プリフライトは宛先が CORS に対応しているかを確かめる仕組みで、本人が望んだかは確かめない。フォームの範囲の要求は確認なしで届く。
  • 線がフォームに合わせて決められた経緯は 一次ソースで確かめていない。言えるのは現行の文言まで。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

Fetch Standard(2026-09-21 版)で、Content-Type が CORS-safelisted request-header になりうる MIME type の essence はどれですか。

用語集(4語)
プリフライト (CORS-preflight request)
本体の要求の前にブラウザが自動で送る OPTIONS 要求。宛先が CORS に対応しているかを確かめる。許可が返らなければ本体は送られない。
CORS-safelisted method
Fetch §2.2.1 の定義で `GET`・`HEAD`・`POST` の3つ。これ以外のメソッドはプリフライトの対象。
CORS-safelisted request-header
プリフライト無しで付けられるヘッダ。対象は accept・accept-language・content-language・content-type・range で、値ごとに条件がある。1つの値は128バイトまで、合計は1024バイトまで。
単純リクエスト
プリフライト無しで送られる要求の通称。取得した版の Fetch Standard 本文には `simple request` という語は見当たらず、判定は safelisted method と safelisted request-header の条件で書かれている。