セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説

kento_morota 14分で読めます
社内のAI・IT活用、技術がわかる相手に無料相談。 相談 →
目次

「脆弱性診断で『セキュリティヘッダーが不足している』と指摘されたが、どれを付ければ何が防げるのかわからない」「CSPを入れたら画面が真っ白になって慌てて外した」「HSTSは付けるだけと聞いたが、preloadの欄が怖くて触れない」。セキュリティヘッダーは設定自体は数行ですが、効果と副作用を理解せずに入れると、守るどころかサービスを止めてしまいます。

この記事では、主要なセキュリティヘッダーがそれぞれどの攻撃を防ぐのかを仕組みから説明し、影響の大きいCSPをReport-Onlyから段階的に導入する手順、nonceとhashの使い分け、HSTSのpreloadが「解除できない」リスク、Nginx・Next.js・Astroでの設定例、securityheaders.comでの確認方法までを解説します。XSSやCSRFの脆弱性そのものへの対策はXSS・CSRF対策入門、CORSはCORSの仕組みと設定で扱っているため、ここではヘッダーによる多層防御に絞ります。

セキュリティヘッダーとは何を防ぐものか

「ブラウザへの指示」で被害を限定する

セキュリティヘッダーは、サーバーがレスポンスに付けてブラウザに「このページをどう扱うべきか」を指示するものです。サーバー側の脆弱性を直すものではなく、脆弱性が突かれたときの被害を限定する、あるいは通信経路や埋め込みに関する攻撃をブラウザ側で拒否させる役割を持ちます。アプリのコードを直すのが一次対策、ヘッダーはそれを補う二次防御という位置づけです。

ヘッダー防ぐ攻撃・問題推奨値の例
Content-Security-PolicyXSSの影響軽減(不正スクリプトの実行阻止)、データ持ち出し先の制限後述(サイトごとに設計)
Strict-Transport-SecurityHTTPへのダウングレード・中間者攻撃max-age=31536000; includeSubDomains
X-Frame-Options / CSPのframe-ancestorsクリックジャッキング(iframe内に埋め込んで誤クリックさせる)DENY または SAMEORIGIN
X-Content-Type-OptionsMIMEスニッフィング(画像として置いたファイルをスクリプト実行)nosniff
Referrer-PolicyURLに含まれるIDやトークンの外部サイトへの漏えいstrict-origin-when-cross-origin
Permissions-Policyカメラ・位置情報など不要なブラウザ機能の無効化camera=(), microphone=(), geolocation=()

それぞれの仕組み

クリックジャッキングは、攻撃者のページに透明なiframeであなたのサイトを重ね、ユーザーが「送信」ボタンを押したつもりで別の操作をさせる手口です。X-Frame-Optionsは自サイトのページをiframeに入れてよいかをブラウザに指示し、DENYなら一切の埋め込みを拒否します。CSPのframe-ancestorsは同じ目的の後継で、埋め込みを許可するオリジンを個別に指定できます。

MIMEスニッフィングは、Content-Typeが曖昧なときにブラウザが中身を推測して解釈する挙動です。ユーザーがアップロードした「画像」に実はHTMLやスクリプトが含まれていた場合、推測によって実行されてしまうことがあります。nosniffは推測を禁止し、宣言されたContent-Typeどおりにしか扱わせません。

HSTSは「このドメインには今後HTTPSでしか接続するな」とブラウザに記憶させます。ユーザーがhttp://で打っても、ブラウザが通信前にhttpsへ書き換えるため、初回以降は平文の通信が一切発生せず、公衆Wi-Fiなどでのダウングレード攻撃を防ぎます。

CSPを段階的に導入する

CSPが「真っ白」を起こす理由

Content-Security-Policy(CSP)は、ページが読み込んでよいスクリプト・スタイル・画像・接続先などのオリジンをホワイトリストで指定するヘッダーです。XSSが起きても、許可されていない場所からのスクリプトやインラインスクリプトはブラウザが実行を拒否するため、被害を大幅に減らせます。その反面、自サイトが使っているGoogle AnalyticsやタグマネージャーやCDN上のフォント、inlineで書かれたイベントハンドラまですべて把握していないと、正規の機能まで止まります。これが「入れたら画面が壊れた」の正体です。

Report-Onlyで違反を集めてから強制する

  1. Content-Security-Policy-Report-Onlyヘッダーで、ブロックはせず違反レポートだけを送らせる。
  2. レポートの送信先(report-toまたはreport-uri)を用意し、数日〜数週間分の違反を集める。自前のエンドポイントでもよいし、レポート収集SaaSを使ってもよい。
  3. 違反のうち正規のものをポリシーに追加し、インラインスクリプトはnonceかhashに置き換える。
  4. 違反がほぼ出なくなったら、ヘッダー名をContent-Security-Policyに変えて強制モードにする。
# 段階1:Report-Onlyで様子を見る(Nginx)
add_header Content-Security-Policy-Report-Only
  "default-src 'self'; script-src 'self' https://www.googletagmanager.com; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'none'; report-uri /csp-report" always;

# 段階2:強制モードに切り替え
add_header Content-Security-Policy
  "default-src 'self'; script-src 'self' 'nonce-$request_id' https://www.googletagmanager.com; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;

nonceとhashの使い分け

'unsafe-inline' を許可するとインラインスクリプト全般が通ってしまい、XSS対策としてのCSPはほぼ無意味になります。代わりに、正規のインラインスクリプトだけを通す手段が2つあります。

  • nonce:リクエストごとにランダムな値を生成し、ヘッダーとscriptタグの両方に付ける。SSRやテンプレートエンジンで動的にHTMLを生成できる場合に向く。値は毎回変える必要があり、静的HTMLやCDNキャッシュとは相性が悪い。
  • hash:インラインスクリプトの内容のSHA-256などをヘッダーに列挙する。内容が固定の静的サイトに向くが、スクリプトを1文字変えるたびに再計算が必要。

Next.jsのようにビルド時にインラインスクリプトが差し込まれるフレームワークはnonceを前提にした仕組みを用意しており、Astroのような静的サイトではhashか、そもそもインラインを使わない設計が現実的です。

HSTSとpreloadの「解除できない」リスク

preloadリストに載るとブラウザに焼き込まれる

HSTSは初回アクセス時にヘッダーを受け取って初めて効くため、「最初の1回」は無防備です。これを埋めるのがpreloadで、hstspreload.orgに申請すると、ChromeやFirefoxなどのブラウザ本体にドメインが組み込まれ、初回からHTTPSが強制されます。申請条件は、max-ageが1年以上、includeSubDomainsとpreloadディレクティブがあること、そしてすべてのサブドメインがHTTPSで応答できることです。

問題は解除の難しさです。リストからの削除申請は可能ですが、各ブラウザのリリースに反映されて全ユーザーに行き渡るまで数か月以上かかります。その間、HTTPでしか動かない社内ツールのサブドメインや、証明書を用意していない古いサブドメインは、preloadに載った瞬間からブラウザに拒否されます。includeSubDomains+preloadは、そのドメイン配下のすべてを永続的にHTTPS化する覚悟を決めてから付けてください。まずはpreloadなし・短いmax-age(数日)で運用し、問題がなければmax-ageを1年に伸ばし、それでも問題がなければpreloadを検討する、という順番が安全です。

Nginx・Next.js・Astroでの設定例と確認

Nginxで一括付与する

アプリケーションに手を入れずに全レスポンスへ付与できるため、まずはリバースプロキシで設定するのが手軽です。add_headerはlocationブロックに1つでもadd_headerがあると上位の設定を継承しないため、共通のincludeファイルにまとめて各locationで読み込むのが安全です。NginxでのHTTPS化が済んでいる前提の設定です。

# /etc/nginx/snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

# server / location 側
server {
    listen 443 ssl;
    include snippets/security-headers.conf;
    location /api/ {
        include snippets/security-headers.conf;   # ここで再includeしないと消える
        proxy_pass http://app;
    }
}

Next.jsとAstroでの設定

// next.config.js(全パスに付与)
const securityHeaders = [
  { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
];
module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};
// CSPのnonceはmiddleware.tsで生成し、x-nonceヘッダー経由でレイアウトに渡す
// Astro(Node/SSRアダプタ利用時): src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (_ctx, next) => {
  const res = await next();
  res.headers.set('X-Content-Type-Options', 'nosniff');
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  return res;
});
// 静的ビルドの場合はミドルウェアが動かないので、配信側(Nginx・CloudFront・Cloudflare)で付与する

設定の確認

設定後は securityheaders.com にURLを入力すると、各ヘッダーの有無と評価が表示されます。手元では curl -I で確認できます。CSPはブラウザの開発者ツールのConsoleに違反が出るので、主要な画面を一通り操作して赤字が出ないか見てください。

curl -sI https://example.com/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'

トラブル事例:CSP導入後に決済フォームが動かなくなった

症状:CSPを強制モードにした翌日から、決済ページのカード入力欄が表示されず、購入が止まった。開発環境では問題なかった。
原因:決済サービスのカード入力フォームはiframeで埋め込まれ、さらにそのスクリプトを外部ドメインから読み込んでいた。Report-Only期間中にテストした画面に決済ページが含まれておらず、frame-srcとscript-srcに決済サービスのオリジンが入っていなかった。開発環境では決済をモックしていたため再現しなかった。
対処:ブラウザのConsoleに出た違反メッセージから必要なオリジンを特定し、frame-srcとscript-srcとconnect-srcに追加。あわせてReport-Onlyの収集期間中に「全画面・全操作を網羅するチェックリスト」を作り、決済・外部認証・チャットウィジェットなど外部埋め込みのある画面を必ず含めるルールにした。強制モード切り替え後も一定期間はreport-uriを残し、本番で違反が出たらSlackに通知するようにしています。

まとめ

セキュリティヘッダーは、脆弱性が突かれたときの被害をブラウザ側で限定する二次防御です。nosniff・X-Frame-Options・Referrer-Policy・Permissions-Policyは副作用がほぼなくすぐに付けられ、HSTSはmax-ageを短くから始めてpreloadは慎重に、CSPはReport-Onlyで違反を集めてnonce/hashに置き換えてから強制する、という順序で進めてください。Nginxで一括付与するか、フレームワークの設定で付けるかは構成次第ですが、いずれもsecurityheaders.comとcurlで結果を確認する習慣をセットにします。Webアプリのセキュリティ強化を体系的に進めたい場合は、Harmonic Societyのシステム開発・インフラ支援にご相談ください。

#CSP#HSTS#セキュリティヘッダー#Webセキュリティ

Harmonic Society

この記事の内容、自社の業務でも活かせそうですか?

ローカルLLM・AI・クラウドなどの技術導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK。まずは30分の無料相談から。売り込みはしません。

共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

関連記事

Related / 9 articles

  1. プログラミング

    DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用

    DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。

  2. プログラミング

    WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド

    WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。

  3. プログラミング

    クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計

    クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。

  4. プログラミング

    秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除

    APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。

  5. プログラミング

    開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御

    開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。

  6. プログラミング

    ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方

    NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。

  7. プログラミング

    ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方

    ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。

  8. プログラミング

    localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決

    localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。

  9. プログラミング

    GitOps入門|ArgoCD・FluxでGitをインフラの正とするデプロイ運用

    GitOpsのPush型とPull型の違い、差分検知と自動同期の仕組み、リポジトリ構成、ArgoCDの最小導入手順までを解説。git revertで戻せるデプロイ運用が理解でき、Kubernetes以外への応用の考え方もわかります。

Harmonic Society

「読んで終わり」にせず、自社の業務で試してみませんか?

AI・ローカルLLM・クラウドの導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK・売り込みなし。

無料・30分・オンラインOK|1営業日以内に返信します