Fetch Metadata Request Headers (Sec-Fetch-*) って何?


はじめに

Web アプリケーションは一般的に多くのエンドポイントにリクエストを行い、時には、機密情報をリクエストに使用したりします。機密情報を扱う際は注意深くリクエストを行う必要がありますが、シンプルなCSRF対応などでは、全て防ぐことはできません。こういった攻撃から大事な情報を守るために Fetch Metadata Request Headers をリクエストに付与します。

各ブラウザでの対応状況はこちらです。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Sec-Fetch-Site#Browser_compatibility

<picture> エレメントでのリクエストでは、

Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site

ユーザのクリックなどのアクションによる同じドメインへのリクエストでは、

Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

がリクエストヘッダに付与されるようになりました。

Sec-Fetch-Dest とは

Sec-Fetch-Dest では、リクエスト先の情報が何なのかを指定します。

指定できるのは、以下の情報になります。

"audio", "audioworklet", "document", "embed", "empty",
"font", "frame", "iframe", "image", "manifest",
"object", "paintworklet", "report", "script","serviceworker",
"sharedworker", "style", "track", "video", "worker", "xslt"

例えば・・

// <img> タグの場合
Sec-Fetch-Dest: image

// new Worker() の場合
Sec-Fetch-Dest: worker

// <iframe>  の場合
Sec-Fetch-Dest: iframe

このヘッダは、

  1. 信頼できる URL かどうかを調べます。
  2. 構造化ヘッダをトークンにします。
  3. リクエスト先情報が空文字だったら、 "empty" を、それ以外だったら リストに沿って、決定します。
  4. 決定したものは、 Sec-Fetch-Dest に設定されます。

Sec-Fetch-Mode とは

Sec-Fetch-Dest では、どういったリクエストなのかを指定します。

指定できるのは、以下の情報になります。

"cors", "navigate", "no-cors", "same-origin", "websocket"

このヘッダは、

  1. 信頼できる URL かどうかを調べます。
  2. 構造化ヘッダをトークンにします。
  3. リストに沿って、決定します。
  4. 決定したものは、 Sec-Fetch-Mode に設定されます。

Sec-Fetch-Site とは

Sec-Fetch-Site では、リクエスト元とリクエスト先の関係を指定します。

指定できるのは、以下の情報になります。

"cross-site", "same-origin", "same-site", "none"

このヘッダは、

  1. 信頼できる URL かどうかを調べます。
  2. 構造化ヘッダをトークンにします。
  3. 値を決定
    • 自身のアプリケーションで生成されたリクエストの場合 "same-origin"
    • 自身のサイトのサブドメインで生成されたリクエストの場合 "same-site"
    • ユーザの操作(ブックマークを選択など)で生成されたリクエストの場合 "none"
    • 別のサイトのリクエストの場合 "cross-site"
  4. 決定したものは、 Sec-Fetch-Mode に設定されます。

Sec-Fetch-User とは

Sec-Fetch-User では、リクエストがナビゲーションリクエスト("document", "embed", "frame", "iframe", "object" のいずれか)によるものかどうかを指定します。(boolean値になります)

このヘッダは、

  1. 信頼できる URL かどうかを調べます。
  2. ナビゲーションリクエストではない場合は、 false となり終了します。
  3. 構造化ヘッダをトークンにします。
  4. ヘッダに true を設定します。
  5. 決定したものは、 Sec-Fetch-User に設定されます。

どうやって、 Web アプリケーションを守るのか

1. Fetch Metadata Request Headers がないリクエストについては、無視して許可します。

全てのサイトでサポートされているわけではないので設定されているものだけチェックするようにする

サンプル
if not req['sec-fetch-site']:
  return True  # Allow this request

2. 自身のサイトからのリクエストブラウザで作られたリクエストのみ許可します。

クロスサイトのリクエストは許可すべきではありません。
サブドメインからのリクエストも許可しない場合は、 "same-site" も拒否するようにすると良いです。

サンプル
if req['sec-fetch-site'] in ('same-origin', 'same-site', 'none'):
  return True  # Allow this request

3. トップレベルナビゲーションと iframe を許可します。

サイトを他のサイトからリンクさせるようにするために許可する必要があります。

サンプル
if req['sec-fetch-mode'] == 'navigate' and req.method == 'GET'
  # <object> and <embed> send navigation requests, which we disallow.
  and req['sec-fetch-dest'] not in ('object', 'embed'):
    return True  # Allow this request

4. クロスサイトでも必要なものは除外します。(任意)

サイトが別のサイトにリソースを提供している場合があります。(favicon.icoなど)
その場合は、クロスサイトでも許可してあげる必要があります。

サンプル
if req.path in ('/my_CORS_endpoint', '/favicon.png'):
  return True

5. その他のリクエストは、拒否します。

ここまでで True に入らなかったリクエストについては全て拒否します。

おわりに

いかがだったでしょうか?
より強固な Web サイトを作成するためには、こうした仕様も駆使して作成したいですね。
ただ、現状、全てのブラウザで対応されているわけではなさそうなので、その点も注意して、実装するようにしましょう。

参考

https://asnokaze.hatenablog.com/entry/2019/02/15/013557
https://w3c.github.io/webappsec-fetch-metadata/
https://web.dev/fetch-metadata/
https://asnokaze.hatenablog.com/entry/2019/12/17/014522