静的なランディングページに問い合わせフォームを置き、送信先だけを 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-Policy や Sec-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;
}
ここで大事なのは、Origin や Sec-Fetch-Site を過信しないことです。
これらは、ブラウザー由来のリクエストを判断する材料としては有用です。
しかし、curl や bot からの直接投稿では、ヘッダーを偽装できます。
そのため、API 側ではヘッダー検証に加えて、必ずサーバー側バリデーションを行います。
たとえば、次のような項目を検証します。
| 項目 | 検証内容 |
|---|---|
name |
必須、最大文字数 |
email |
必須、メールアドレス形式、最大文字数 |
message |
必須、最大文字数 |
Content-Type |
許可した形式のみ |
| 本文サイズ | 上限を超えたリクエストを拒否 |
HTML の required や type="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 側では、Origin、Referer、Sec-Fetch-Site、Sec-Fetch-Mode、Content-Type などを見て、入口を絞ります。
ただし、ヘッダーは偽装される可能性があります。
最終的には、API 側で次の対策を組み合わせる必要があります。
- サーバー側バリデーション
- 送信回数制限
- メール送信制限
- ログ記録
- エラー情報の制限
Baseline は、主要ブラウザーでその機能が利用できるかを見るための互換性の目安です。
セキュリティヘッダーを採用するかどうかは、Baseline だけでなく、守りたい対象、フォームの構成、ログイン有無、外部リソースの利用状況によって決める必要があります。
未ログイン LP フォームのセキュリティは、次の一文で整理できます。
静的 LP 側では、ヘッダーでページのふるまいを制限する。
フォーム API 側では、ヘッダーで入口を絞り、入力値と送信処理をサーバー側で検証する。
CSRF トークンを入れるかどうかだけで考えるのではなく、LP と API の境界で何を制限し、どこから先をアプリケーション側で検証するのかを整理することが重要です。