結論(危険なJavaScript APIの兆候)
危険なJavaScript APIの兆候とは、ページのHTMLに次のような「シンク」の記述があることです。innerHTML・outerHTMLへの代入、insertAdjacentHTML()、document.write()、eval()、new Function()、文字列を渡したsetTimeout()/setInterval()が該当します。信頼できない値が流れ込むとDOMベースXSSになるため、textContentやJSON.parse()などの安全なAPIへ置き換えます。記述があるだけでは脆弱性と確定しないため、セキュリティAIチェッカーは△(要確認)と判定します。
この記事のポイント
- innerHTMLなどのHTML書き込みAPIとeval()などのコード実行APIは、DOMベースXSSの「シンク」です。
- innerHTMLで挿入したscript要素は実行されませんが、onerror属性付きのimg要素などでコードは実行されます。
- location.hashのようにサーバーへ送られない値が使われるため、サーバー側のエスケープやWAFでは防げません。
- textContent、insertAdjacentText()、JSON.parse()、関数を渡すsetTimeout()が安全な代替です。
- CSPのrequire-trusted-types-for 'script'(Trusted Types)を、まずReport-Onlyで試してから強制すると再発を防げます。
- セキュリティAIチェッカーはトップページHTML内の6種類のパターンを確認し、1つでも一致すれば△と判定します。
| 診断項目名 | 危険なJavaScript APIの兆候 |
|---|---|
| 項目ID | html.dangerous-js |
| 分類 | HTML・JavaScript・外部資産 |
| 重大度・配点の重み | 重要(high) 重み 3 |
| 関連する標準・ガイドライン | OWASP ASVS 5.0.0 V1.3.2(L1: eval()など動的コード実行機能を避ける)、V3.2.2(L1: テキストとして表示する内容はtextContent等の安全な関数で扱う)、V1.2.3(L1: JavaScript生成時の出力エンコーディング)/OWASP WSTG-CLNT-01「DOM-Based Cross Site Scripting」、WSTG-CLNT-02「JavaScript Execution」/CWE-79(クロスサイト・スクリプティング)、CWE-95(動的に評価されるコードへのインジェクション)/IPA「安全なウェブサイトの作り方」5. クロスサイト・スクリプティング |
危険なJavaScript APIの兆候とは
危険なJavaScript APIの兆候とは、Webページのコードに、文字列をHTMLやプログラムとして解釈させるブラウザのAPI(シンク)が書かれていることです。代表的なものは次の2種類です。
- 文字列をHTMLとして解釈してDOMに書き込むAPI:
innerHTML、outerHTML、insertAdjacentHTML()、document.write() - 文字列をプログラムとして実行するAPI:
eval()、new Function()、文字列を渡したsetTimeout()/setInterval()
これらは「シンク(sink)」と呼ばれます。URLのクエリやハッシュ、postMessage で受け取ったデータ、APIの応答などの信頼できない値がシンクに流れ込むと、サーバー側にまったく痕跡を残さずにスクリプトが実行される「DOMベースXSS」の原因になります。
// 危険なシンクの典型例(値の出どころがURLなら即DOMベースXSS)
resultArea.innerHTML = location.hash.slice(1);
セキュリティAIチェッカーのこの項目は、トップページのHTML(インラインスクリプトを含む)にこうした危険なシンクの記述があるかを文字列パターンで確認します。記述があること自体は脆弱性の証明ではなく、「入力値が流れ込んでいないかをコードレビューすべき箇所がある」という兆候として扱います。
シンクとソースの関係
DOMベースXSSは、攻撃者が操作できる値の入口(ソース)から、危険な出口(シンク)まで値が届いたときに成立します。
| 区分 | 例 |
|---|---|
| ソース(攻撃者が操作できる値) | URLのクエリ、location.hash、postMessage で受け取ったデータ、APIの応答 |
| シンク(HTMLとして書き込む) | innerHTML、outerHTML、insertAdjacentHTML()、document.write()/writeln() |
| シンク(コードとして実行する) | eval()、new Function()、文字列を渡した setTimeout()/setInterval() |
対策の基本は、ソースを無害化することよりも、シンクそのものを安全なAPIに置き換えることです。
危険なJavaScript APIはなぜ危険なのか
innerHTMLは「XSS攻撃の最も一般的な経路」
MDNの innerHTML の解説では、このプロパティが「おそらくXSS攻撃の最も一般的な経路」であると説明されています。innerHTML で挿入された <script> 要素は実行されませんが、<img src=x onerror=...> のようにイベントハンドラー属性を持つ要素を差し込めば、コードは実行されます。つまり「scriptタグを除去したから安全」という考え方は成り立ちません。
OWASPが挙げる「信頼できないデータを入れてはならない」メソッド
OWASPのDOM based XSS Prevention Cheat Sheetは、信頼できないデータを入れてはならないメソッドとして、innerHTML、outerHTML、document.write()、document.writeln() を挙げています。また、暗黙的にコードを実行するものとして eval()、文字列を渡した setTimeout()/setInterval()、new Function() を挙げ、「正しい出力メソッド(シンク)を使うことがDOMベースXSSを直す最善の方法」としています。
OWASP ASVS 5.0.0の要求
- V1.3.2(L1):
eval()などの動的コード実行機能を避けること。やむを得ず使う場合は、入力を事前にサニタイズすること。 - V3.2.2(L1):HTMLとしてではなくテキストとして表示すべき内容を、
createTextNodeやtextContentで扱うこと。 - V1.2.3(L1):JavaScriptを生成するときの出力エンコーディング。
いずれもレベル1(最低限満たすべき水準)の要件です。脆弱性の分類としてはCWE-79(クロスサイト・スクリプティング)とCWE-95(動的に評価されるコードへのインジェクション)に当たり、OWASP WSTGではWSTG-CLNT-01「DOM-Based Cross Site Scripting」とWSTG-CLNT-02「JavaScript Execution」で検査します。IPA「安全なウェブサイトの作り方」でも、5. クロスサイト・スクリプティングとして対策が求められています。
危険なAPIと安全な代替の対応表
| 危険なシンク | 主な代替 |
|---|---|
el.innerHTML = 文字列 | el.textContent = 文字列、document.createElement()+append() |
insertAdjacentHTML() | insertAdjacentText()、insertAdjacentElement() |
document.write()/writeln() | DOM APIによる要素生成、<script async> での外部読込 |
eval(文字列) | JSON.parse()、関数テーブル(オブジェクトのキーで関数を選ぶ) |
setTimeout("処理", n) | setTimeout(() => 処理(), n) |
new Function(文字列) | あらかじめ定義した関数の呼び出し |
HTMLを意図的に挿入する必要がある場合(CMSの本文表示など)は、DOMPurifyのような実績あるサニタイザーを通したうえで挿入します。さらにCSPの require-trusted-types-for 'script'(Trusted Types)で文字列の直接代入を禁止すると、シンクの使用箇所を強制的に一元化できます。Trusted Typesの適用状況は「Trusted Typesの適用」の項目で別に確認します。
危険なJavaScript APIについてよくある誤解
- 「サーバー側でエスケープしているのでDOMベースXSSは起きない」:DOMベースXSSでは
location.hashのようにサーバーへ送られない値が使われることがあり、サーバー側のエスケープでは防げません。 - 「innerHTMLの読み取りも危険」:危険なのは代入(書き込み)です。
const s = el.innerHTML;のような読み取りだけでは実行は起きません。 - 「CSPがあればeval()は問題ない」:CSPで
'unsafe-eval'を許可していなければeval()は実行時にブロックされますが、CSPが無い、または'unsafe-eval'を許可している環境では保護になりません。CSPの設定は「CSPでunsafe-evalを許可していない」の項目で確認します。
なお、onclick などHTML属性に書かれたイベントハンドラーは「危険なインラインイベントを抑制」、postMessage の送信先・受信元の検証は「postMessageの送受信元を限定」の各項目で確認します。このページは、HTMLやコードを文字列から生成するAPIの記述に絞って解説します。
危険なJavaScript APIの兆候の診断基準(セキュリティAIチェッカー)
取得対象
- 診断URLへのGETで取得したトップページ(リダイレクト後の最終URL)のHTML本文(最大2MB)。
- 対象はHTML内の文字列のみで、
<script src>で読み込まれる外部JavaScriptファイルの中身は取得・解析しません。 - HTMLが取得できない(非テキスト応答)場合は、
html.availableが対象外となり、本項目は出力されません。
検出パターン(大文字小文字を区別)
| ラベル | 正規表現 | 意図 |
|---|---|---|
| eval() | \beval\s*\( | retrieval( のような語中一致は除外 |
| new Function() | \bnew\s+Function\s*\( | Functionコンストラクター |
| document.write() | \bdocument\.write(?:ln)?\s*\( | writeln も対象 |
| innerHTML/outerHTMLへの代入 | \.(?:inner|outer)HTML\s*\+?=(?!=) | 代入と += のみ。読み取りや === 比較は除外 |
| insertAdjacentHTML() | \.insertAdjacentHTML\s*\( | HTML挿入API |
| 文字列引数のsetTimeout/setInterval | \bset(?:Timeout|Interval)\s*\(\s*["'`] | 第1引数が文字列リテラルの場合のみ |
判定条件
| 判定 | 条件 | 得点への影響 |
|---|---|---|
| ○ 問題なし(pass) | 上記パターンがいずれも一致しない | 重み3を満点で加算 |
| △ 要確認(warn) | 1つ以上一致(根拠欄に一致したラベルを列挙) | 3×0.45=1.35点 |
| × 要改善(fail) | 使用しない(静的な兆候のみでは脆弱性を確定できないため) | — |
| 対象外(na) | 使用しない(HTMLが取得できれば必ず判定) | — |
スコアは「Σ(重み×係数)÷Σ(適用項目の重み)×100」で計算され、係数はpass=1.00、warn=0.45、fail=0です。△の場合、根拠欄に「innerHTML/outerHTMLへの代入、document.write()の文字列を検出しました。」のように一致したラベルが表示されるので、それを手がかりにコードレビューを始められます。
自治体サイトでの判定例
| トップページの状態 | 判定 |
|---|---|
CMSが出力するトップページで、スライダーやお知らせタブの切り替えに el.innerHTML = html; を使うインラインスクリプトがある | △(「innerHTML/outerHTMLへの代入」) |
古いアクセス解析タグが document.write('<img src=...>') を含む | △ |
処理がすべて外部 .js ファイルにあり、HTML内のインラインスクリプトは設定値のみ | ○(ただし外部ファイル内は未確認) |
診断の限界
文字列一致による静的な兆候判定のため、本文中のコード解説記事や、ミニファイ・難読化されたコードでは誤検知・見逃しが起こり得ます。外部JavaScriptの中身、実行時のデータフロー(入力値がシンクへ届くか)は確認していません。○は「トップページHTMLに代表的なシンクの記述が見当たらない」ことだけを意味します。
危険なJavaScript APIを放置するとどんなリスクがあるか
攻撃シナリオ(location.hashをinnerHTMLへ代入している場合)
- 検索ページのインラインスクリプトが
location.hashの値をresultArea.innerHTMLへそのまま代入している。 - 攻撃者は
https://www.city.example.lg.jp/search.html#<img src=x onerror=...>のようなURLをメールやSNSで配布する。 - 住民や職員がリンクを開くと、自治体ドメイン上で攻撃者のスクリプトが実行され、表示内容の改ざん(偽の給付金案内など)、入力フォームの内容の窃取、ログイン中の管理画面に対する操作などが行われる。
ハッシュ部分はサーバーへ送信されないため、WAFやアクセスログでは検知しにくいのが特徴です。攻撃が起きても、サーバー側の記録からは何が起きたのかを追跡できない可能性があります。
想定される被害
- 公式サイト上での偽情報表示による住民の誤誘導やフィッシング
- 問い合わせ・申請フォームに入力された個人情報の外部送信
- 同じオリジンで動く職員向け機能(CMSプレビュー等)の悪用
- 公式サイトの信頼性低下と、インシデント対応・公表に伴う業務負荷
自治体の公式ドメインで表示される偽の案内は、住民にとって本物と見分けがつきません。シンクの記述が残っているだけでもレビューの対象にすべき理由はここにあります。
危険なJavaScript APIの直し方(安全なAPIへの置き換え・CSP・Trusted Types)
1. 使用箇所の棚卸し
まず、公開中のHTMLとソースツリーの両方から危険なシンクの記述を洗い出します。
# 公開中のHTMLを取得し、危険なシンクの記述を確認
curl -s https://www.city.example.lg.jp/ | grep -nE 'innerHTML|outerHTML|insertAdjacentHTML|document\.write|eval\(|new Function|set(Timeout|Interval)\(\s*["'"'"']'
# ソースツリー全体(テンプレート・外部JS)も確認
grep -rnE 'innerHTML\s*\+?=|insertAdjacentHTML|document\.write|eval\(' ./htdocs ./templates
各箇所について「代入される値の出どころ」を確認し、URL・フォーム入力・外部APIの応答・postMessage 由来の値が入る箇所を優先して修正します。診断では外部JavaScriptの中身を見ていないため、ソースツリー側の確認は特に重要です。
2. 安全なAPIへの置き換え
テキストとして表示すればよい箇所は textContent と要素生成に、文字列を実行している箇所は関数やJSON.parse()に置き換えます。
// 修正前
notice.innerHTML = '<p>' + params.get('msg') + '</p>';
// 修正後(テキストとして表示)
const p = document.createElement('p');
p.textContent = params.get('msg') ?? '';
notice.replaceChildren(p);
// 修正前
setTimeout("showSlide(" + n + ")", 3000);
// 修正後
setTimeout(() => showSlide(n), 3000);
// 修正前
const data = eval('(' + jsonText + ')');
// 修正後
const data = JSON.parse(jsonText);
HTMLの挿入が業務上必要な場合は、サニタイザーを通します。
import DOMPurify from 'dompurify';
article.innerHTML = DOMPurify.sanitize(htmlFromCms);
3. CSPとTrusted Typesで再発を防ぐ
コードを直しても、改修や外部タグの追加で再びシンクが持ち込まれることがあります。CSPとTrusted Typesでブラウザ側に制約をかけると再発を防げます。まず報告専用モード(Report-Only)で影響を確認し、問題がなければ強制モードへ移します。
Apache(.htaccess)
# Apache (.htaccess) 例: まずReport-Onlyで観測
<IfModule mod_headers.c>
Header always set Content-Security-Policy-Report-Only "script-src 'self'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script'"
</IfModule>
nginx
# nginx 例
add_header Content-Security-Policy-Report-Only "script-src 'self'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script'" always;
IIS(web.config)
<!-- IIS web.config 例 -->
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Content-Security-Policy-Report-Only" value="script-src 'self'; object-src 'none'; base-uri 'none'" />
</customHeaders>
</httpProtocol>
</system.webServer>
Next.js(Vercel)
Next.js(Vercel)では next.config.js の headers()、またはnonceを使う場合はmiddleware(proxy)でCSPを付与します。
// next.config.js
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: [
{ key: 'Content-Security-Policy-Report-Only', value: "script-src 'self'; object-src 'none'; base-uri 'none'" },
]}];
},
};
4. 修正後の確認方法
- 修正後、
curl -s URL | grep -cE 'innerHTML\s*\+?=|document\.write'で記述が減ったことを確認する。 - ブラウザの開発者ツールで、CSP違反(Report-Only)のコンソール出力が出ないことを確認する。
- 本診断を再実行し、根拠欄のラベルが消えていることを確認する。
サイトを壊さないための注意点
'unsafe-eval'を削除したCSPやTrusted Typesを、いきなり強制モードで全ページに適用しないでください。古いjQueryプラグインやアクセス解析タグが動かなくなることがあります。必ずReport-Onlyで観測期間を設けます。- CMSのテンプレートを修正する場合は、全ページ共通部分に影響するため、検証環境で主要テンプレート(トップ、一覧、記事、フォーム)を確認してから反映します。
- 外部ベンダー提供のスクリプト(翻訳、読み上げ、チャットボット等)は自庁で直接修正できないため、契約・仕様書で安全なAPIの使用を求めます。
よくある質問(FAQ)
innerHTMLはなぜ危険なのですか?
代入した文字列がHTMLとして解釈されるため、信頼できない値が入ると攻撃者のコードが実行されるからです。MDNはinnerHTMLを、おそらくXSS攻撃の最も一般的な経路と説明しています。script要素は実行されませんが、onerror属性を持つimg要素などではコードが動きます。
innerHTMLを読み取るだけでも危険ですか?
危険なのは代入(書き込み)です。const s = el.innerHTML; のような読み取りだけではコードは実行されません。セキュリティAIチェッカーも、代入と += だけを検出し、読み取りや === の比較は対象外にしています。
eval()の代わりに何を使えばよいですか?
JSONを読み込む目的ならJSON.parse()を使います。処理を切り替えたい場合は、オブジェクトのキーで関数を選ぶ関数テーブルにします。文字列を渡したsetTimeout()は、setTimeout(() => 処理(), n) のように関数を渡す形に書き換えます。
サーバー側でエスケープしていればDOMベースXSSは起きませんか?
防げない場合があります。DOMベースXSSではlocation.hashのようにサーバーへ送信されない値が使われることがあり、サーバー側のエスケープやWAF、アクセスログでは検知も防御もできません。ブラウザ側で安全なAPIを使う必要があります。
CSPを設定していればeval()は問題になりませんか?
CSPで'unsafe-eval'を許可していなければeval()は実行時にブロックされます。ただし、CSPが無い、または'unsafe-eval'を許可している環境では保護になりません。
危険なJavaScript APIの兆候が診断で△になったら、脆弱性があるということですか?
確定ではありません。この項目は文字列パターンによる静的な兆候の確認で、記述があることは「入力値が流れ込んでいないかをコードレビューすべき箇所がある」という意味です。そのためfail(×)は使わず、warn(△、重み3×0.45=1.35点)としています。
外部の.jsファイルの中身も診断されますか?
されません。対象はトップページHTML(インラインスクリプトを含む)の文字列だけで、script srcで読み込む外部JavaScriptの中身は取得・解析していません。passでも外部ファイルは別途確認が必要です。
この項目を含む127項目を、URLを入れるだけで無料診断します。