• 診断項目 No.10/127
  • コンテンツセキュリティポリシー(CSP)
  • 重大度 重要(high)

CSPでunsafe-inlineを許可していないか?危険な理由・無効になる条件・nonce/hashへの移行方法

CSPでunsafe-inlineを許可していないかは、CSPがXSS対策として機能するかを左右します。unsafe-inlineが危険な理由、nonce・hash・strict-dynamicで無視される条件、nonce/hashへの移行手順と診断基準を解説します。

結論(CSPでunsafe-inlineを許可していない)

CSPでunsafe-inlineを許可していないとは、Content Security Policyのスクリプト用・スタイル用のディレクティブで 'unsafe-inline' が有効になっていない状態です。'unsafe-inline' はHTMLに直接書いた script 要素、onclick などのイベントハンドラー属性、javascript: URL、インラインのスタイルを許可します。XSSの典型的な注入はインラインのスクリプトのため、スクリプトで 'unsafe-inline' が有効なCSPはXSS対策としての効果をほぼ失います。外部ファイル化やnonce・hashで外せます。

この記事のポイント

  • スクリプトの 'unsafe-inline' は注入された script 要素やイベントハンドラーをそのまま実行させるため、最も優先して外すべき設定です。
  • 同じソースリストにnonce・hash(スクリプトでは 'strict-dynamic' も)があれば、'unsafe-inline' はCSP Level 3対応ブラウザで無視されます。
  • script-src も default-src もないCSPは、インラインスクリプトを無制限に許可しているのと同じです。
  • セキュリティAIチェッカーはスクリプトで有効なら×、スタイルだけ有効なら△、どちらも無効なら○と判定します。
  • インラインコードの棚卸し→外部ファイル化→残りをnonce/hash化→Report-Onlyで確認→強制、の順に進めます。
診断項目名CSPでunsafe-inlineを許可していない
項目IDheaders.csp-inline
分類コンテンツセキュリティポリシー(CSP)
重大度・配点の重み重要(high) 重み 3
関連する標準・ガイドラインOWASP ASVS 5.0.0 V3.4.3(L2:許可リストまたはnonce/hashを定義。L3はレスポンスごとのnonce/hash)、OWASP WSTG-CONF-12、IPA「安全なウェブサイトの作り方」1.5 XSS、CWE-79

CSPでunsafe-inlineを許可していないとは

CSPでunsafe-inlineを許可していないとは、Content Security Policy(CSP)のスクリプト用・スタイル用のディレクティブで、'unsafe-inline' というキーワードが実際には有効になっていない状態のことです。

'unsafe-inline' は、次のようなHTMLに直接書かれたコードの実行・適用を許可するキーワードです。

  • HTML内に直接書かれた <script>...</script>
  • onclick="..." などのイベントハンドラー属性
  • javascript: で始まるURL
  • インラインの <style> 要素や style 属性

XSS(クロスサイト・スクリプティング)の典型的な注入は、まさに「インラインのスクリプト」です。そのため、スクリプトに対して 'unsafe-inline' が有効なCSPは、XSS対策としての効果をほぼ失います。

# 危険:インラインスクリプトがすべて実行される
Content-Security-Policy: default-src 'self' 'unsafe-inline' https:

セキュリティAIチェッカーのこの項目は、実際にブラウザで有効になっているスクリプト用・スタイル用の 'unsafe-inline' を評価します。文字列として書かれているだけで、nonce・hash・'strict-dynamic' によって無効化されている場合は問題としません。CSPそのものの有無は「Content Security Policy」で確認します。

unsafe-inlineはどのディレクティブで効くのか(仕組み)

有効なディレクティブの決まり方

インラインのコードに適用されるディレクティブは、種類ごとに次の順で決まります(CSP3 §6.8.2)。

インラインの種類適用されるディレクティブ(左から優先)
<script> 要素・javascript: URLscript-src-elem → script-src → default-src
イベントハンドラー属性(onclick 等)script-src-attr → script-src → default-src
<style> 要素style-src-elem → style-src → default-src
style 属性style-src-attr → style-src → default-src

script-src も default-src もなければ、インラインスクリプトは無制限です。たとえば frame-ancestors 'self' だけのCSPは、インラインスクリプトをすべて許可しているのと同じです。

'unsafe-inline' が無視される条件(CSP Level 3)

W3C CSP3 の「Does a source list allow all inline behavior for type?」アルゴリズムでは、同じソースリストに次のいずれかがあれば、'unsafe-inline' は効果を持ちません。

  • nonce-source('nonce-xxxx')または hash-source('sha256-...'、'sha384-...'、'sha512-...')
  • スクリプトの場合は 'strict-dynamic'

この仕様により、Googleなどが推奨する「strict CSP」では、古いブラウザ向けの後方互換として 'unsafe-inline' https: を併記しつつ、対応ブラウザではnonceだけが有効になる書き方が使われます。

Content-Security-Policy: script-src 'nonce-R4nd0m' 'strict-dynamic' 'unsafe-inline' https:; object-src 'none'; base-uri 'none'

スクリプトとスタイルで危険度が違う理由

  • スクリプトの 'unsafe-inline':注入された <script> やイベントハンドラーがそのまま実行されます。Mozilla HTTP Observatory はこれを「安全でない実装」として大きく減点(-20)します。
  • スタイルの 'unsafe-inline':スクリプトの実行には直結しませんが、CSSインジェクションによる画面の偽装や、属性セレクターを悪用した情報の外部送信の余地を残します。Observatory では style-src だけの 'unsafe-inline' は減点0の扱いです。一方、OWASPのCSPチートシートは 'unsafe-inline' に頼らないことを推奨しています。

unsafe-inlineについてよくある誤解

  • 「'unsafe-inline' を消すとサイトが壊れる」:インラインの <script> と onclick 属性を外部ファイル+addEventListener に移すか、nonce/hashを付ければ維持できます。
  • 「nonceを全scriptタグに自動付与するミドルウェアで済む」:OWASPチートシートは、攻撃者が注入したscriptにもnonceが付いてしまうため、出力後のHTMLを書き換えて一律に付与する方式を避けるよう警告しています。

なお、HTML本文に onclick などのインラインイベントがどれだけ残っているかは「危険なインラインイベントを抑制」、文字列をコードとして実行する eval の許可は「CSPでunsafe-evalを許可していない」で別に確認します。

CSPでunsafe-inlineを許可していないの診断基準(セキュリティAIチェッカー)

取得対象

  • トップページの最終応答の Content-Security-Policy ヘッダーを取得します。複数本ある場合は、カンマで分割して個別のポリシーとして扱います。
  • HTML内のmeta CSPも対象にします。
  • 各ポリシーを ; で分割し、ディレクティブ名・値を小文字化して評価します。
  • 同名のディレクティブが重複した場合は、CSP仕様と同じく最初のものを採用します。

判定ロジック

  1. 各ポリシーについて、<script> 要素用のリスト(script-src-elem→script-src→default-src の順で最初にあるもの)と、イベントハンドラー属性用のリスト(script-src-attr→script-src→default-src)を求めます。
  2. リストが存在しない(無制限)、または 'unsafe-inline' を含み、かつnonce/hash/'strict-dynamic' を含まない場合、そのポリシーはその種類のインライン実行を許可しているとみなします。
  3. 複数のポリシーは、すべてのポリシーで許可されたときだけ実行されます。そのため、要素用・属性用のどちらかについてすべてのポリシーが許可している場合に「インラインスクリプト許可」とします。
  4. スタイルも同様に、style-src-elem/style-src-attr→style-src→default-src の順で評価します。nonce/hashがあれば 'unsafe-inline' は無効とみなし、'strict-dynamic' はスタイルには影響しません。スタイル用のリスト(default-src を含む)を持つポリシーが1つ以上あることを条件とします。

判定条件

判定条件得点への影響
× 要改善(fail)スクリプトのインライン実行が有効('unsafe-inline' が有効、または script-src・default-src がなく無制限)0点
△ 要確認(warn)スクリプトは制限されているが、スタイルで 'unsafe-inline' が有効3×0.45=1.35点
○ 問題なし(pass)スクリプト・スタイルとも 'unsafe-inline' が有効ではない3点
対象外(na)CSPがヘッダーにもmetaにもない(「Content Security Policy」で×済み)採点に計上しない

自治体サイトでの判定例

CSP判定
default-src 'self' 'unsafe-inline' https:×
frame-ancestors 'self' のみ×(スクリプト無制限)
script-src 'self'; style-src 'self' 'unsafe-inline'△
script-src 'nonce-abc' 'strict-dynamic' 'unsafe-inline' https:○(unsafe-inlineは無視される)
default-src 'self'; script-src 'self' 'sha256-...'○
script-src 'self'; script-src-attr 'unsafe-inline'×(onclick 等の属性が実行可能)

unsafe-inlineを許可しているとどんなリスクがあるか

  • 格納型XSS:お知らせ・イベント情報・掲示板など、職員や外部委託先が入力した内容を表示するページでエスケープ漏れがあると、<script> や <img src=x onerror=...> が保存され、閲覧した住民全員のブラウザで実行されます。'unsafe-inline' が有効だと、CSPはこれを止めません。
  • 反射型XSS:検索結果ページで検索語をそのまま表示する実装では、細工したURLを踏ませるだけでスクリプトが動きます。
  • 想定される被害:偽の申請フォームやフィッシングページへの差し替え、入力内容の外部送信、職員が管理画面を開いた場合のセッションの悪用など。

CSPを入れていても、スクリプトの 'unsafe-inline' が有効なままでは、こうした攻撃に対しては「CSPがない」のとほとんど変わりません。

unsafe-inlineを外す方法(nonce・hash・Apache・nginx・Next.js)

移行の手順

  1. インラインコードの棚卸し:<script> の中身、onclick/onload 等の属性、javascript: リンク、style 属性を洗い出します。
  2. 外部ファイル化:可能なものは .js/.css ファイルへ移し、イベントは addEventListener で登録します。
  3. 残るインラインはnonceまたはhash:動的ページはリクエストごとに乱数のnonce、静的ページは内容のSHA-256ハッシュを使います。
  4. Report-Onlyで確認してから強制:違反がないことを確認してから強制します。
  5. スタイルは後回しでも可:スクリプトの 'unsafe-inline' の除去を優先します。

イベントハンドラー属性の置き換え例

<!-- 変更前 -->
<button onclick="openMenu()">メニュー</button>
<!-- 変更後 -->
<button id="menu-btn">メニュー</button>
<script src="/js/menu.js"></script>
// /js/menu.js
document.getElementById('menu-btn').addEventListener('click', openMenu);

Apache(静的サイトでhashを使う例)

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-BASE64ハッシュ値'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
</IfModule>

ハッシュは、<script> と </script> の間の文字列そのもの(空白・改行を含む)から計算します。1文字でも変わると一致しなくなります。

printf '%s' 'console.log("hello")' | openssl dgst -sha256 -binary | openssl base64

nginx(インラインを外部ファイル化した静的サイトの例)

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;

nginxの静的配信だけでは、nonceを毎回生成できません。nonce方式にする場合は、アプリ(PHP等)側でCSPヘッダーとscriptタグに同じnonceを出力し、nginx側ではCSPを付けないでください。両方で付けると2つのポリシーが同時に強制され、nginx側の script-src 'self' がnonce付きのインラインスクリプトを遮断します。

PHP(動的ページでnonceを使う例)

<?php
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'nonce-{$nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'");
?>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES) ?>" src="/js/app.js"></script>

IIS

web.config の customHeaders は固定値のため、nonceはASP.NET等のアプリ側で付与し、IISではhash方式または外部ファイル化を基本にします。

Next.js(Vercel)

proxy.ts で crypto.randomUUID() から毎リクエストnonceを作り、script-src 'self' 'nonce-…' 'strict-dynamic' を設定すると、Next.jsがフレームワークのスクリプトに自動でnonceを付けます(全ページの動的レンダリングが必要)。静的生成を維持したい場合は、実験的なSRI(experimental.sri)とハッシュの組み合わせも公式に案内されています。開発時のみ 'unsafe-eval' が必要な点も公式ガイドに記載があります。

CMS・外部タグ(Googleタグマネージャー等)

Googleタグマネージャー等はインラインの初期化スクリプトを使うため、nonceを付けるか、hash化します。タグマネージャー経由で追加される任意のスクリプトは、'strict-dynamic' の下では信頼が伝播するため、タグ管理の権限の管理も重要です。

設定後の確認方法

curl -sI https://www.example.lg.jp/ | grep -i '^content-security-policy'

script-src(script-src-elem・script-src-attr があればそれぞれ。なければ default-src)に 'unsafe-inline' があり、同じリストにnonce/hash/'strict-dynamic' がない場合は要修正です。ブラウザのコンソールで「Refused to execute inline script」が出ないことも確認します。

よくある質問(FAQ)

CSPのunsafe-inlineはなぜ危険なのですか?

XSSの典型的な注入はインラインのスクリプトだからです。スクリプトで 'unsafe-inline' が有効だと、攻撃者が注入した script 要素や onerror などのイベントハンドラーをCSPが止めず、ブラウザがそのまま実行します。

script-srcに 'unsafe-inline' と nonce を両方書いても大丈夫ですか?

CSP Level 3に対応したブラウザでは、同じソースリストにnonceまたはhashがあると 'unsafe-inline' は無視されます。Googleなどが推奨するstrict CSPは、古いブラウザ向けの後方互換としてこの書き方を使っており、セキュリティAIチェッカーでも問題にしません。

style-srcの 'unsafe-inline' も外す必要がありますか?

スクリプトほど直接的ではありませんが、CSSインジェクションによる画面の偽装や情報の外部送信の余地を残します。OWASPチートシートは 'unsafe-inline' に頼らないことを推奨しています。優先順位はスクリプトが先で、スタイルは後回しでも構いません。

onclick属性を使っていると 'unsafe-inline' を外せませんか?

外せます。onclick などの属性を外部の .js ファイルに移し、addEventListener でイベントを登録すれば、'unsafe-inline' なしで同じ動作を維持できます。

全scriptタグにnonceを自動付与するミドルウェアを使えば十分ですか?

おすすめできません。出力後のHTMLを書き換えて一律にnonceを付けると、攻撃者が注入したscriptにもnonceが付いてしまうため、OWASPチートシートはこの方式を避けるよう警告しています。

nginxだけでnonceを使えますか?

nginxの静的配信だけではリクエストごとにnonceを生成できません。nonce方式はPHP等のアプリ側でCSPヘッダーとscriptタグに同じnonceを出力し、nginx側ではCSPを付けないようにします。静的サイトでは外部ファイル化かhash方式が基本です。

セキュリティAIチェッカーを利用する

この項目を含む127項目を、URLを入れるだけで無料診断します。

参考資料