Skip to content

セキュリティヘッダを _headers に追加して MDN Observatory の grade を上げる #149

Description

@newt239

MDN HTTP Observatory のスキャン結果(scan id 120446917 / algorithm v6 / 2026-09-11)で grade C- / score 45(12 件中 4 件 fail) だった。Worker スクリプトを持たない構成のため、対応手段は public/_headers に限られる。その範囲で直せるものを整理する。

現状のスキャン結果

テスト 結果 増減 _headers で対応可能
content-security-policy ❌ csp-not-implemented -25 ✅
x-frame-options ❌ x-frame-options-not-implemented -20 ✅
x-content-type-options ❌ x-content-type-options-not-implemented -5 ✅
subresource-integrity ❌ sri-not-implemented-but-external-scripts-loaded-securely -5 ❌(HTML 側の変更、かつ実質対応不可)
referrer-policy ⚪ 未設定(減点なし・加点機会あり) 0 → +5 ✅
cross-origin-opener-policy ⚪ 未設定(減点なし・加点機会あり) 0 → +10 ✅
cross-origin-resource-policy ⚪ 未設定(減点なし・加点機会あり) 0 → +10 ✅
strict-transport-security ✅ hsts-preloaded +5 —
redirection ✅ redirection-all-redirects-preloaded 0 —
cookies / cross-origin-resource-sharing / cross-origin-embedder-policy ✅ 0 —

HSTS と redirection は .dev TLD 自体が HSTS preload list に載っているため既にパスしている。http://newt239.dev/ は現状 200 を返す(リダイレクトしない)が、preload により実ブラウザは HTTP で到達しないのでスコア上は問題にならない。

やること

1. 追加するヘッダ(public/_headers の /*)

  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Resource-Policy: same-site
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  • X-Content-Type-Options と Referrer-Policy は副作用がない
  • Cross-Origin-Opener-Policy: same-origin は window.open 先との参照を切る。外部リンクは target="_blank" のみで opener に依存していないため影響なし
  • Cross-Origin-Resource-Policy は OG 画像など他サイトからの埋め込みを制限する。クローラ(X / Slack など)はブラウザではないので CORP を解釈せず OG 画像には影響しないが、same-origin ではなく same-site を選んで影響範囲を狭める
  • Strict-Transport-Security はスコアには効かない(既に preload で加点済み)が、preload list への依存を明示的に外す意味で入れる

2. CSP の導入

減点幅が最大(-25)かつ x-frame-options-implemented-via-csp(+5)も同時に取れる。ただしインラインスクリプト・インラインスタイルの扱いで到達できる水準が変わる。

段階 A: 'unsafe-inline' 込みで導入する(-25 → -20、XFO は -20 → +5)

  Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; manifest-src 'self'; script-src 'self' 'unsafe-inline' https://use.typekit.net https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://use.typekit.net https://p.typekit.net; font-src 'self' https://use.typekit.net https://p.typekit.net; img-src 'self' data: https://img.newt239.dev https://i.scdn.co https://www.googletagmanager.com https://*.google-analytics.com; connect-src 'self' https://api.newt239.dev https://p.typekit.net https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com

許可オリジンの根拠:

段階 B: script-src から 'unsafe-inline' を外す(-20 → 0、default-src 'none' 込みなら +10)

style-src だけの 'unsafe-inline' は Observatory の満点扱い(csp-implemented-with-unsafe-inline-in-style-src-only)なので、スタイル側はそのままでよい。script 側に必要なのは次の 2 つ。

  • nuxt.config.ts の onload="Typekit.load({async:true})" というインラインイベントハンドラを外す。ハンドラ属性は hash では許可できず 'unsafe-hashes' が必要になるため、クライアントプラグインからの読み込みへ移す
  • プリレンダ HTML に残るインラインスクリプト 3 種(<script type="importmap">、window.__NUXT_SITE_CONFIG__、window.__NUXT__.config)を hash で許可する。いずれもページ間で同一なので、ビルド後に .output/public から sha256 を計算して _headers を生成するスクリプトで賄える。<script type="application/ld+json"> は CSP の対象外

段階 B はビルド生成物への依存が生まれるので、段階 A を先に入れて実害がないことを確認してから着手する。

3. 対応しないもの

  • subresource-integrity (-5): 外部スクリプトは Typekit の kit JS と gtag.js の 2 本。どちらも配信側が内容を更新するため integrity を固定するとサイトが壊れる。_headers の範囲外でもあり対応しない

見込み

段階 A まで(1 + 2A)で score 45 → 90 前後、段階 B まで実施すると 100 / A+ に届く。

検証

  • ローカルで pnpm run generate → pnpm run serve:static し、CSP 違反が出ないことを DevTools の Console で確認する
  • 本番反映後に Observatory を再スキャンして grade を確認する
  • CSP は最初 Content-Security-Policy-Report-Only で入れて違反の有無を見てから昇格させてもよい

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions