#はじめに

「このリクエストは正規のユーザー操作から来たのか、それとも攻撃者のスクリプトから来たのか?」——サーバー側でこれを判断できたら、多くの攻撃を防げます。

Fetch Metadata Headersは、ブラウザがリクエストのコンテキスト情報を自動的に付与するヘッダーです。サーバーはこの情報を使って、不審なリクエストを拒否できます。

この記事を読むと、以下のことができるようになります:

  • Fetch Metadata Headersの各ヘッダーの意味を理解できる
  • サーバー側でリクエストを検証する方法がわかる
  • CSRF攻撃やデータ窃取を防ぐ実装ができる

#Fetch Metadata Headersとは

Fetch Metadata Headersは、ブラウザがHTTPリクエストに自動的に付与する、リクエストのコンテキスト情報を含むヘッダーです。

すべてのヘッダー名はSec-Fetch-で始まります。このプレフィックスは、JavaScriptから偽装できないことを保証します。

GET /api/user/123 HTTP/1.1
Host: api.example.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Sec-Fetch-User: ?0

#主要なヘッダー

#Sec-Fetch-Site

リクエスト元とリクエスト先の関係を示します。

意味
same-origin同一オリジン
same-site同一サイト(サブドメイン含む)
cross-site別サイト
noneユーザー操作(アドレスバー入力、ブックマーク)

#判定例

リクエスト元: https://www.example.com
リクエスト先: https://api.example.com

→ Sec-Fetch-Site: same-site(同一サイト)
リクエスト元: https://evil.com
リクエスト先: https://api.example.com

→ Sec-Fetch-Site: cross-site(別サイト)

#Sec-Fetch-Mode

リクエストのモードを示します。

意味
navigateナビゲーション(ページ遷移)
corsCORSリクエスト(fetch, XHR)
no-corsCORSなしリクエスト(img, script読み込み)
same-origin同一オリジンリクエスト
websocketWebSocket接続

#Sec-Fetch-Dest

リクエストされたリソースの用途を示します。

用途
documentHTMLドキュメント
scriptJavaScriptファイル
styleCSSファイル
image画像
fontフォント
emptyfetch/XHRなど
iframeiframe

#Sec-Fetch-User

ユーザー操作によって発生したリクエストかどうかを示します。

意味
?1ユーザー操作あり(クリック、キー入力など)
?0ユーザー操作なし

#サーバー側での活用

#Resource Isolation Policy

クロスサイトからのリソース読み込みを拒否します。

def check_fetch_metadata(request):
    site = request.headers.get('Sec-Fetch-Site')
    mode = request.headers.get('Sec-Fetch-Mode')
    dest = request.headers.get('Sec-Fetch-Dest')

    # 同一オリジン/サイトからのリクエストは許可
    if site in ['same-origin', 'same-site', 'none']:
        return True

    # クロスサイトでもナビゲーションは許可(リンクからのアクセス)
    if mode == 'navigate' and dest == 'document':
        return True

    # その他のクロスサイトリクエストは拒否
    return False

#CSRF対策の強化

APIエンドポイントでクロスサイトからのリクエストを拒否します。

@app.route('/api/transfer', methods=['POST'])
def transfer():
    site = request.headers.get('Sec-Fetch-Site')

    # クロスサイトからのPOSTを拒否
    if site not in ['same-origin', 'same-site']:
        return jsonify({'error': 'Forbidden'}), 403

    # 通常の処理
    ...

#画像のホットリンク防止

自サイト以外からの画像読み込みを防止します。

@app.route('/images/<path:filename>')
def serve_image(filename):
    site = request.headers.get('Sec-Fetch-Site')
    dest = request.headers.get('Sec-Fetch-Dest')

    # 画像リクエストがクロスサイトからの場合は拒否
    if dest == 'image' and site == 'cross-site':
        return '', 403

    return send_from_directory('images', filename)

#実装パターン

#パターン1: 厳格なAPI保護

if Sec-Fetch-Site != 'same-origin':
    return 403 Forbidden

最も厳格。同一オリジンからのリクエストのみ許可。

#パターン2: サイト内許可

if Sec-Fetch-Site not in ['same-origin', 'same-site']:
    return 403 Forbidden

サブドメイン間の通信を許可。

#パターン3: ナビゲーション許可

if Sec-Fetch-Site == 'cross-site':
    if Sec-Fetch-Mode != 'navigate':
        return 403 Forbidden

外部サイトからのリンクは許可、埋め込みやAPIコールは拒否。

#パターン4: CORS併用

if Sec-Fetch-Mode == 'cors':
    # CORSヘッダーで制御
    return handle_cors_request()
else:
    if Sec-Fetch-Site == 'cross-site':
        return 403 Forbidden

CORSで許可したリクエストはそのまま、それ以外のクロスサイトは拒否。

#制限と注意点

#ブラウザサポート

Fetch Metadata Headersは比較的新しい仕様です。古いブラウザはこれらのヘッダーを送信しません。

def check_fetch_metadata(request):
    site = request.headers.get('Sec-Fetch-Site')

    # ヘッダーがない場合(古いブラウザ)
    if site is None:
        # フォールバック: 他の方法で検証
        return check_origin_header(request)

    # ヘッダーがある場合は検証
    ...

#偽装不可の保証

Sec-プレフィックスのヘッダーは、JavaScriptから設定できません。

// これはブラウザによって無視される
fetch('/api', {
  headers: {
    'Sec-Fetch-Site': 'same-origin'  // 無視される
  }
});

ブラウザが自動的に正しい値を設定します。

#サーバー側の対応必須

Fetch Metadata Headersは、ブラウザが情報を提供するだけです。サーバー側で検証ロジックを実装しない限り、セキュリティ効果はありません。

#DevToolsで確認

  1. DevToolsのNetworkタブを開く
  2. 任意のリクエストを選択
  3. HeadersタブでSec-Fetch-*ヘッダーを確認
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?0

#ミドルウェア実装例

#Express.js

function fetchMetadataPolicy(req, res, next) {
  const site = req.headers['sec-fetch-site'];
  const mode = req.headers['sec-fetch-mode'];
  const dest = req.headers['sec-fetch-dest'];

  // ヘッダーがない場合(古いブラウザ)はスキップ
  if (!site) {
    return next();
  }

  // 同一オリジン/サイトは許可
  if (site === 'same-origin' || site === 'same-site' || site === 'none') {
    return next();
  }

  // ナビゲーション(リンククリック)は許可
  if (mode === 'navigate' && dest === 'document') {
    return next();
  }

  // その他は拒否
  return res.status(403).json({ error: 'Forbidden' });
}

app.use('/api', fetchMetadataPolicy);

#まとめ

  • Fetch Metadata Headersはリクエストのコンテキスト情報を提供
  • Sec-Fetch-Site: リクエスト元との関係(same-origin/cross-site等)
  • Sec-Fetch-Mode: リクエストのモード(navigate/cors等)
  • Sec-Fetch-Dest: リソースの用途(document/script/image等)
  • サーバー側で検証ロジックを実装する必要がある
  • 他のセキュリティ対策と組み合わせて使用

#次のステップ

オリジンとセキュリティモジュールはこれで完了です。同一オリジンポリシー、CORS、CSP、セキュリティヘッダー、そしてFetch Metadata Headersについて学びました。

次のモジュールでは、キャッシュ戦略について学びます。HTTPキャッシュの仕組み、Cache-Controlディレクティブ、CDNキャッシュなど、パフォーマンス最適化の基礎を身につけましょう。

#参考リンク