0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

静的 LP フォームを守るセキュリティヘッダー入門

0
Posted at

静的なランディングページに問い合わせフォームを置き、送信先だけを API にする構成はよくあります。

たとえば、Astro で LP を静的生成し、フォーム送信だけを /api/contact に送るような構成です。

この記事では、未ログインの LP フォームを題材に、セキュリティヘッダーの考え方を整理します。
主題は、静的 LP 側ではレスポンスヘッダーでページのふるまいを制限し、フォーム API 側ではリクエストヘッダーを見て入口を絞ることです。

対象読者

この記事は、次のような人を対象にしています。

  • 静的 HTML / CSS / JavaScript でランディングページを制作している人
  • 問い合わせフォームだけを API に送信する構成を考えている人
  • CSRF、CSP、Origin、Referer、Sec-Fetch-* などの名前は聞いたことがあるが、LP フォームで何を優先すべきか整理したい人
  • Web アプリケーション全体の高度なセキュリティ設計ではなく、小規模な LP フォームの最小防御セットを知りたい人

この記事では、ログイン機能、管理画面、会員情報変更、Cookie セッションを前提にした Web アプリケーションは扱いません。
また、WAF、bot 対策、Rate Limiting、外部フォームサービスなど、外部サービスに任せる対策も省略します。


1. 未ログイン LP フォームで優先したい防御

CSRF は、典型的にはログイン済みユーザーのブラウザーを悪用して、本人が意図していない操作を対象サイトに実行させる攻撃です。

そのため、CSRF が問題になりやすいのは、ブラウザーが Cookie などの認証情報を自動送信する場面です。

たとえば、次のような操作では CSRF 対策が重要になります。

操作 CSRF トークンの重要度
ログイン後のプロフィール変更 高い
管理画面での記事削除 高い
予約内容の変更 高い
注文・退会・決済関連処理 高い
未ログインの問い合わせフォーム 相対的に低い

未ログインの問い合わせフォームでは、ログイン済み Cookie を悪用して「本人の権限で状態変更させる」という構図が弱くなります。

その代わり、次のような問題が主な関心になります。

  • 別サイトから勝手にフォーム API に投稿される
  • bot やスクリプトから直接 POST される
  • 想定外の Content-Type で送られる
  • 不正な入力値が送られる
  • メール送信機能が悪用される
  • LP が別サイトに iframe で埋め込まれる
  • 外部スクリプトや外部送信先が増えすぎる

つまり、未ログイン LP フォームでは、CSRF トークンだけを考えるのではなく、LP と API の境界で何を制限するかを考える必要があります。


2. Baseline はセキュリティ基準ではなく互換性の目安

セキュリティヘッダーを調べていると、MDN などで Baseline という表示を見かけることがあります。

Baseline は、Web Platform の機能が主要ブラウザーでどれだけ利用可能かを示す互換性の目安です。

つまり、Baseline が答える問いは次です。

この機能は、主要ブラウザーで使えるのか?

一方で、セキュリティ設計で考えるべき問いは別です。

この LP / API に、そのヘッダーを設定する意味があるのか?

この2つを混同しないことが重要です。

観点 問い
Baseline 主要ブラウザーで使えるか
セキュリティ設計 この構成で設定する意味があるか

たとえば、Content-Security-PolicySec-Fetch-Site のように、広く利用できる機能もあります。
しかし、Baseline で広く利用可能だからといって、それだけで安全になるわけではありません。

逆に、Baseline ではない、または一部ブラウザーで対応差があるからといって、必ず不要になるわけでもありません。

セキュリティヘッダーは、Baseline を互換性確認の材料として見つつ、守りたい対象、対象ブラウザー、フォームの構成、外部リソースの利用状況に合わせて採用するものです。


3. 静的 LP 側:CSP を中心にレスポンスヘッダーを設定する

静的 LP 側では、主にレスポンスヘッダーを設定します。

目的は、ページ上で次のような挙動を制限することです。

  • どこから JavaScript を読み込めるか
  • どこから画像やフォントを読み込めるか
  • fetch() でどこへ接続できるか
  • <form> でどこへ送信できるか
  • 他サイトから iframe で埋め込めるか
  • 外部サイトへ Referer をどこまで送るか

最小セットとしては、次のようなヘッダーを検討します。

ヘッダー 目的
Content-Security-Policy 読み込み元・送信先・埋め込みを制限する
Strict-Transport-Security HTTPS 接続を強制する
X-Content-Type-Options MIME type sniffing を抑制する
Referrer-Policy 外部に送る参照元情報を制御する
Permissions-Policy 不要なブラウザー機能を閉じる
X-Frame-Options クリックジャッキング対策の補助

この中で、LP フォームの文脈で中心に置きたいのは Content-Security-Policy です。

たとえば、単純な LP なら次のような CSP から考えられます。

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data: https:;
  connect-src 'self';
  form-action 'self';
  base-uri 'self';
  object-src 'none';
  frame-ancestors 'none';

X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: DENY

LP フォームでは、特に次の3つが重要です。

ディレクティブ LP フォームでの意味
connect-src fetch() の送信先を制限する
form-action <form> の送信先を制限する
frame-ancestors 他サイトからの iframe 埋め込みを制限する

connect-src は JavaScript の fetch() などによる接続先を制限します。
form-action は HTML フォームの送信先を制限します。
frame-ancestors は、他サイトから LP を iframe などで埋め込まれることを制限します。

現代的なクリックジャッキング対策では、CSP の frame-ancestors を中心に考えます。
X-Frame-Options: DENY は、互換性や慣習上の補助として併記されることがあります。


4. API 側:Origin / Sec-Fetch-Site / Content-Type を見る

CSP は、あくまで「その LP ページ上で何ができるか」を制限するものです。

しかし、攻撃者はあなたの LP を経由せず、直接 /api/contact に POST することもできます。
そのため、フォーム API 側ではリクエストヘッダーを見て、想定外の入口を拒否します。

見るべき代表的なヘッダーは次のとおりです。

ヘッダー 用途
Origin 想定した LP からの送信か確認する
Referer 補助的に送信元ページを確認する
Sec-Fetch-Site cross-site などを判定する
Sec-Fetch-Mode navigate / cors などを判定する
Content-Type 想定した形式だけ受け付ける

たとえば、フォーム API 側では次のような検証を考えられます。

const allowedOrigins = new Set(['https://example.com']);

function validateRequestHeaders(request: Request) {
  const origin = request.headers.get('Origin');
  const secFetchSite = request.headers.get('Sec-Fetch-Site');
  const contentType = request.headers.get('Content-Type') ?? '';

  if (origin && !allowedOrigins.has(origin)) return false;
  if (secFetchSite === 'cross-site') return false;

  const allowedContentType =
    contentType.includes('application/json') ||
    contentType.includes('application/x-www-form-urlencoded');

  if (!allowedContentType) return false;

  return true;
}

ここで大事なのは、OriginSec-Fetch-Site を過信しないことです。

これらは、ブラウザー由来のリクエストを判断する材料としては有用です。
しかし、curl や bot からの直接投稿では、ヘッダーを偽装できます。

そのため、API 側ではヘッダー検証に加えて、必ずサーバー側バリデーションを行います。

たとえば、次のような項目を検証します。

項目 検証内容
name 必須、最大文字数
email 必須、メールアドレス形式、最大文字数
message 必須、最大文字数
Content-Type 許可した形式のみ
本文サイズ 上限を超えたリクエストを拒否

HTML の requiredtype="email" は、ユーザー体験を改善するためのものです。
API を守る最終防衛線にはなりません。


5. ヘッダーでは守れないもの

セキュリティヘッダーは重要ですが、万能ではありません。

ヘッダーで守れるものと、守れないものを分けて考える必要があります。

脅威 ヘッダーだけで守れるか
他サイトへの送信先の制限 ある程度守れる
外部スクリプトの読み込み制限 ある程度守れる
iframe 埋め込みによるクリックジャッキング ある程度守れる
ブラウザー由来の cross-site POST ある程度検出できる
bot による直接 POST 守れない
大量投稿 守れない
不正な入力値 守れない
メール送信機能の悪用 守れない
API キー漏洩 守れない

たとえば、Sec-Fetch-Site: cross-site を拒否しても、ヘッダーを偽装する bot を止められるわけではありません。
Content-Type を制限しても、正しい Content-Type で不正な値を送られれば意味がありません。
CSP で form-action を制限しても、攻撃者が直接 API に POST することは別問題です。

そのため、フォーム API 側では、少なくとも次を考える必要があります。

  • サーバー側バリデーション
  • 送信回数制限
  • メール送信回数の制限
  • ログ記録
  • エラー内容を詳しく返しすぎない
  • メールヘッダーインジェクション対策
  • 入力値の最大長制限

特に問い合わせフォームでは、メール送信処理に注意が必要です。

フォーム API は、最終的に管理者や営業担当者へメールを送ることが多いです。
つまり、攻撃者から見ると「メール送信機能を外部から叩ける入口」になります。

そのため、次のような制御も必要です。

対策 理由
宛先をユーザー入力で決めない 第三者へのメール送信に悪用されるのを防ぐ
件名や From にユーザー入力を直接使わない ヘッダーインジェクションを防ぐ
本文に入れる値を適切に処理する 表示崩れや意図しない解釈を防ぐ
入力値に最大文字数を設定する 長大な本文による負荷や悪用を抑える
内部エラーをそのまま返さない 実装情報の漏えいを防ぐ

ヘッダーは「入口を絞る」ためのものです。
フォーム処理そのものを安全にするには、API 側の実装が必要です。


6. まとめ:入口を絞り、API 側で検証する

未ログインの LP フォームでは、CSRF トークンだけを考えても十分ではありません。

ログイン済み Cookie を前提にした管理画面や会員画面なら、CSRF トークンは重要です。
しかし、未ログインの問い合わせフォームでは、典型的な CSRF よりも、次の問題が先に来ます。

  • 想定外の投稿元から送られる
  • bot から直接 POST される
  • 不正な入力値が送られる
  • メール送信機能が悪用される
  • LP が意図しない外部リソースや送信先を持つ

そのため、静的 LP 側では、CSP を中心にレスポンスヘッダーを設定します。

API 側では、OriginRefererSec-Fetch-SiteSec-Fetch-ModeContent-Type などを見て、入口を絞ります。

ただし、ヘッダーは偽装される可能性があります。
最終的には、API 側で次の対策を組み合わせる必要があります。

  • サーバー側バリデーション
  • 送信回数制限
  • メール送信制限
  • ログ記録
  • エラー情報の制限

Baseline は、主要ブラウザーでその機能が利用できるかを見るための互換性の目安です。
セキュリティヘッダーを採用するかどうかは、Baseline だけでなく、守りたい対象、フォームの構成、ログイン有無、外部リソースの利用状況によって決める必要があります。

未ログイン LP フォームのセキュリティは、次の一文で整理できます。

静的 LP 側では、ヘッダーでページのふるまいを制限する。
フォーム API 側では、ヘッダーで入口を絞り、入力値と送信処理をサーバー側で検証する。

CSRF トークンを入れるかどうかだけで考えるのではなく、LP と API の境界で何を制限し、どこから先をアプリケーション側で検証するのかを整理することが重要です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?