説明
リクエストのパラメーターやヘッダーなど、信頼できない入力を出力先の文脈に合わせてエンコードせずHTML応答へ挿入すると、反射型XSSが発生します。ブラウザーが値をHTMLやJavaScriptとして解釈し、攻撃者のコードが動作するおそれがあります。検索結果、エラーメッセージ、プロフィール画面などに<script> やイベントハンドラーが反映されると、読み取り可能な認証情報の窃取、なりすまし、リダイレクトなどにつながります。
想定される影響
- 読み取り可能なCookieやトークンを盗まれ、アカウントを乗っ取られるおそれがあります。
- ページの権限で任意のJavaScriptを実行される可能性があります。
- 偽のログインフォームやリダイレクトにより、フィッシング被害につながるおそれがあります。
- 画面の改ざんやフォームの自動送信で、意図しない操作を行わされる可能性があります。
- メールアドレスやCSRFトークンなど、ページ内の機密情報を外部へ送信されるおそれがあります。
対処方法
- HTML本文、属性、URLなど、出力位置に合うエンコードを行ってください。URLはスキームと宛先も検証してください。HTMLサニタイズはマークアップを許可するときの処理であり、テキストのエンコードとは異なります。
- Pugなど自動エスケープを行うテンプレートエンジンを使い、信頼できない値をHTML文字列へ直接連結しないでください。
- 可能なら
res.jsonでJSONを返し、Content-Typeを適切に設定してください。クライアントでも応答の値を安全なテキストAPIで表示してください。 - 想定する長さ、パターン、許可する文字を検証してください。
- CSPや
X-Content-Type-Optionsは追加の防御として使い、出力エンコードの代わりにしないでください。 - Express、テンプレートエンジン、エンコード・サニタイズライブラリを更新してください。
例
変更前
javascript
const express = require("express");
const app = express();
// ユーザー入力をHTMLへ直接挿入
app.get("/greet", (req, res) => {
const name = req.query.name || "guest";
// nameが"<img src=x onerror=alert(1)>"の場合、スクリプトが実行される可能性がある
res.type("text/html").send(`<h1>Hello ${name}</h1>`);
});
app.listen(3000);
変更後
javascript
const express = require("express");
const he = require("he"); // HTMLエンコード用ライブラリ
const app = express();
// 出力先の文脈に合わせてエンコード
app.get("/greet", (req, res) => {
const rawName = typeof req.query.name === "string" ? req.query.name : "guest";
const safeName = he.encode(rawName, { useNamedReferences: true }); // &, <, >, ", ' などをエンコード
res.type("text/html").send(`<h1>Hello ${safeName}</h1>`);
});
// またはJSONを返す
app.get("/greet.json", (req, res) => {
const name = String(req.query.name || "guest");
res.json({ message: "Hello", name });
});
app.listen(3000);
解説:
- 変更前: 入力をHTMLへ直接連結しています。挿入されたタグやイベントハンドラーによって悪意のあるJavaScriptが動き、アカウントの悪用やフィッシングにつながる可能性があります。
- 変更後: 応答を返す前に
he.encodeで特殊文字をHTML本文用のエンティティへ変換します。JSONを返す方法でも、クライアント側で値を安全に表示する必要があります。