Securityチーム Web脆弱性診断員の深井です。
今回は、ここ1年以内でよく見かけるようになってきたscript要素にユーザーの入力値が反映されてしまった際に発生するクロスサイト・スクリプティングについて解説していきます。
クロスサイト・スクリプティング(XSS)とは
クロスサイト・スクリプティング(Cross-Site Scripting、以下XSS)とは、Webアプリケーションの脆弱性を悪用し、悪意のあるスクリプトをユーザーのブラウザ上で実行させる攻撃手法です。
本来、ユーザーが入力した値は単なる文字列として扱われるべきですが、入力値の扱い方や出力先に応じた適切な対策が行われていない場合、ブラウザがその入力値をHTMLやスクリプトとして解釈し、意図しないコードを実行してしまうことがあります。
その結果、Cookieやセッション情報の窃取、利用者へのなりすまし、不正サイトへの誘導などの被害が発生するおそれがあります。
一般的なXSS対策としてHTMLエスケープ(※)が広く利用されていますが、出力先によってはHTMLエスケープだけでは十分とはいえません。
本記事では、script 要素内に外部入力を埋め込んだ場合に発生するXSSと、その対策について解説します。
(※)HTMLエスケープの例は以下の通り。
| 文字 | エンティティ | 用途 |
|---|---|---|
& | & | エンティティの開始文字 |
< | < | タグ開始文字 |
> | > | タグ終了文字 |
" | " | ダブルクォーテーション(属性値) |
' | ' または '* | シングルクォーテーション(属性値) |
通常はHTMLエスケープにより対策が可能
Webアプリケーションでは、ユーザーが入力した値を画面へ表示することがよくあります。例えば、ECサイトで商品購入時に住所や名前を入力し、入力された内容を確認画面にて出力するというのがイメージしやすいと思います。
多くの場合、入力値はHTML本文やHTML属性値へ出力されるため、HTMLエンティティエンコードを行うことで、ブラウザに文字列として扱わせることができます。
HTML本文へ出力する場合
例えば、ユーザー名を画面へ表示する場合です。
<?php
$name = $_GET['name'];
?>
<p><?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
この時、悪意あるユーザーが次のような値を入力したとします。
<script>alert(1)</script>
しかし、上記の通り、htmlspecialchars() が適用されているため、ブラウザへは次のようなHTMLが出力されます。
<p><script>alert(1)</script></p>
scriptタグはHTMLタグとしてではなく文字列として扱われるため、スクリプトは実行されません。
HTML属性値へ出力する場合
続いてHTML属性値に反映される場合の例を見ていきます。
<?php
$keyword = $_GET['keyword'];
?>
<input
type="text"
value="<?= htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8') ?>"
>
この時、悪意あるユーザーが次のような値を入力したとします。
" onfocus="alert(1)
これは、value属性の値を最初の"で閉じた上で、イベントハンドラ(onfocus)を追加しており、以下のような形になります。
<input type="text" value="" onfocus="alert(1)"
しかし、同じようにhtmlspecialchars() が適用されているため、ブラウザへは次のようなHTMLが出力されます。
<input
type="text"
value="" onfocus="alert(1)"
>
ダブルクォーテーションがHTMLエスケープされ、value 属性の途中で終了することはありません。
その結果、onfocus 属性は追加されず、入力値は単なる文字列として扱われます。
このように、HTML属性へ外部入力を出力する場合は、htmlspecialchars() などによるHTMLエスケープを行うことで、多くのXSSを防ぐことができます。
「HTMLエスケープしているから安全」は本当か?
XSSの対策にはスクリプトなどの外部入力値をHTMLエスケープすることが一般的ですが、script要素内にユーザーの入力値が反映されるケースでは、HTMLエスケープだけでは対処しきれません。
script要素内で発生するXSSの具体例
まずは例として以下のプログラムを見ていきます。
<script>
var name = "<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>";
</script>
一見すると、htmlspecialchars() によって < や >、"、' がHTMLエンティティへ変換されるため、安全に見えます。
しかし、<script> 要素内で生成されるのはHTMLではなくJavaScriptのコードです。そのため、このコンテキストではHTMLエスケープではなく、JavaScript文字列リテラルとしてのエスケープが必要になります。
例えば、JavaScript文字列リテラルでは引用符("、')、バックスラッシュ(\)、改行などを適切にエスケープしなければ、文字列を途中で終了させ、任意のJavaScriptを実行される可能性があります。
仮に、JavaScript文字列リテラルとしてのエスケープを行わずに外部入力を埋め込んだ場合、悪意あるユーザーが次のような入力をするとします。
"; alert(1); //
すると、生成されるJavaScriptは次のようになります。
var name = "";
alert(1);
//";
このように、文字列を途中で終了させ、任意のJavaScriptが実行される可能性があります。
script要素内でのXSS対策
対策1. 外部入力を script 要素内に直接反映しない
最も有効な対策は、ユーザー入力やリクエストパラメータなどの外部入力を、そのまま script 要素内に埋め込まないことです。htmlspecialchars() を使用していても、script要素内では十分な対策とはいえません。
また、外部入力を画面へ反映する必要がないのであれば、そもそも出力しないことが最も安全です。
さらに、名前やID、カテゴリなど、どのような入力値なのか形式が決まっているのであれば、許可リストや入力値の検証(例:正規表現)を行い、想定外の値を受け付けないようにすることも対策の1つとなります。
対策2. data-* 属性を利用する
外部入力をJavaScriptで利用する必要がある場合は、script要素内へ直接埋め込まず、HTMLの data-*属性に保持し、JavaScriptから取得する方法を推奨します(以下は一例です)。
HTML側では、外部入力をHTMLエンティティエンコードしたうえで data-*属性へ格納します。
<div
id="user"
data-name="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"
></div>
<script src="app.js"></script>
例えば、ユーザーが入力した値が </script><script>alert(1)</script> だった場合、HTMLには次のように出力されます。
<div
id="user"
data-name="</script><script>alert(1)</script>"
></div>
<script src="app.js"></script>
この方法では、外部入力がscript要素内へ埋め込まれないため、</script> によってscript要素が終了する問題を回避できます。
JavaScriptではdatasetを利用してdata-*属性の値を取得します。
window.onload = function () {
var div = document.getElementById("user");
var name = div.dataset.name;
init(name);
};
このように、HTMLからデータを取得することで、JavaScriptコード内へ外部入力を直接埋め込む必要がなくなります。
このようにすることで、HTMLから安全に値を受け渡すことができます。
data-* 属性を利用する際の注意点
対策2では、外部入力をdata-*属性へ格納し、JavaScriptから取得する方法を紹介しました。しかし、この対策を行っても、値を反映する際にinnerHTMLを使用してしまうと再びブラウザによるHTMLとしての解釈が行われ、悪意あるスクリプトが実行される可能性が残ります。
そのため、画面へ文字列として表示したい場合は、innerHTMLではなくtextContentを使用してください。
textContent は文字列をHTMLとして解釈せず、そのままテキストとして表示するため、安全に扱うことができます。
まとめ
script要素内に外部入力を直接埋め込むと、htmlspecialchars()などでHTMLエンティティエンコードを行っていても、</script>によってscript要素が終了し、XSSが発生する可能性があります。
これは、HTMLパーサがJavaScriptの文法を理解しているわけではなく、</script>をscript要素の終了タグとして解釈するためです。
そのため、外部入力をJavaScriptで利用する場合は、次の点を意識しましょう。
- 外部入力を script 要素内へ直接埋め込まない。
- 外部入力をJavaScriptへ渡す必要がある場合は、HTMLエンティティエンコードを行ったうえで data-* 属性などへ保持し、JavaScriptから取得する。
- data-* 属性から取得した値は、HTMLエンティティがデコードされた文字列として取得されるため、そのまま innerHTML へ渡さない。
- 画面へ表示する場合は、innerHTML ではなく textContent など、文字列をテキストとして扱うAPIを利用する。
XSS対策では、「どのように値を渡すか」だけでなく、「取得した値をどのように利用するか」まで含めて考えることが重要です。出力先のコンテキストに応じた適切な処理を行い、安全なWebアプリケーションを実装しましょう。
セキュアなシステム開発についての
\ 無料相談受付中! /

株式会社神戸デジタル・ラボ
デジタルビジネス本部 Securityチーム
2019年に新卒でSIer企業へ入社し、自社プロダクトの開発業務に従事。
2023年4月に神戸デジタル・ラボへ入社し、主にWebアプリケーション脆弱性診断、クラウドセキュリティ設定診断を担当。またPSIRT業務(脆弱性のトリアージ)など診断以外にも携わる。
Udemy講座「脆弱性診断員が教えるAWSクラウドセキュリティ設定トレーニング」の講師を担当。
【参考】
体系的に学ぶ 安全なWebアプリケーションの作り方
https://www.sbcr.jp/product/4797361193


