セキュアコーディング Stored XSSの防止策

開発の現場では「セキュリティは大事」とよく言われますが、正直、実感が湧きにくい時もありますよね。
特に**Stored XSS(ストアドクロスサイトスクリプティング)**は、目に見えづらく、後になって気付くことが多いタイプの脆弱性です。私も以前、レビュー時に見逃しかけて冷や汗をかいたことがあります。

よく「Stored XSSって、SQLインジェクションと同じじゃないの?」と聞かれますが、全く別の攻撃手法です。
この記事では、Stored XSSとは何か、どんなリスクがあるのか、そしてどう防げばいいのかを、自分の実体験や各言語での具体例も交えて紹介します。

1. Stored XSSとは?

Stored XSS(永続型XSS)は、ユーザーの入力データに悪意あるスクリプトが含まれていて、それがそのままデータベースに保存されてしまうことで発生します。
後にそのデータがWebページ上で表示されると、意図せずJavaScriptが実行されてしまいます。

例:

sqlCopyEdit-- 入力内容が保存される INSERT INTO comments (content) VALUES ('<script>alert("XSS")</script>');
-- 表示時にそのまま出力される SELECT content FROM comments;


このように、悪意のあるスクリプトが「普通のデータ」として保存されてしまうと、他のユーザーがページを閲覧した際に、そのブラウザ上で勝手にスクリプトが動いてしまうのです。

2. Stored XSSの危険性
Stored XSSの厄介な点は、一度仕込まれると「ずっとそこに居続ける」こと。主なリスクは以下の通りです:

  • 継続的なリスク:データベースに残ったスクリプトが、何度でも実行される可能性があります。
  • 影響範囲の広さ:投稿者本人だけでなく、その投稿を閲覧する全ユーザーが被害を受ける可能性があります。
  • 検出の難しさ:コードに紛れていると、パッと見では気づきにくいです。
  • 攻撃の深刻度:セッション乗っ取り、Cookieの窃取、CSRFトークンの奪取などにつながります。

私自身も昔、ちょっとしたコメント欄の実装で「ただ表示してるだけだから大丈夫」と思っていたら、実は危険だった…という経験があります。

3. Stored XSSを防ぐには
実際の対策は、「保存時」と「表示時」の両方でエスケープ処理を行うことが基本です。それに加えて、CSPやスキャナなども併用するとより安全です。

入力値のエスケープ処理(保存前)
保存前にHTMLエスケープを行うことで、悪意あるスクリプトを文字列として無害化します。

javaCopyEditString safeContent = Encode.forHtml(userInput);
preparedStatement.setString(1, safeContent);


出力時のエスケープ処理(表示前)
保存時だけでなく、表示前にも再エスケープを行うことで、スクリプトの実行を確実に防ぎます。

javaCopyEditString safeOutput = Encode.forHtml(resultSet.getString("content"));
out.println(safeOutput);

Prepared Statementの使用
XSSとは直接関係ないように思えるかもしれませんが、SQLインジェクションを防ぐ上では重要です。
ただし、**XSS対策としては「これだけでは不十分」**で、必ずHTMLエスケープとセットで使いましょう。

javaCopyEditString query = "INSERT INTO comments (content) VALUES (?)";PreparedStatement stmt = connection.prepareStatement(query);
stmt.setString(1, userInput);
stmt.executeUpdate();

Content Security Policy(CSP)の設定
CSPを使うと、たとえXSSが仕込まれていてもスクリプトの実行をブラウザ側でブロックできます。

htmlCopyEdit<meta http-equiv="Content-Security-Policy" content="default-src 'self';">

セキュリティスキャンの定期実施
ツールを使った定期的な脆弱性チェックも欠かせません。個人的には以下がおすすめです:

  • OWASP ZAP
  • Burp Suite
  • SonarQube(静的解析)

4. 他言語での対策例
PHP

phpCopyEdit$userInput = htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');$query = "INSERT INTO comments (content) VALUES ('$userInput')";mysqli_query($connection, $query);


Python(Flask + SQLite 例)

pythonCopyEditimport html
user_input = request.args.get("name")
safe_input = html.escape(user_input)
cursor.execute("INSERT INTO comments (content) VALUES (?)", (safe_input,))

Node.js

javascriptCopyEditconst escapeHtml = require('escape-html');let userInput = req.query.name;let safeInput = escapeHtml(userInput);
connection.query('INSERT INTO comments (content) VALUES (?)', [safeInput]);

5. 結論
Stored XSSは見落とされがちで、静かに・確実に被害を拡大するタイプの脆弱性です。

押さえておくべきポイントは以下の通り:

  • 保存時・表示時のHTMLエスケープ
  • Prepared Statementの使用(SQLi対策)
  • CSPの導入
  • 脆弱性スキャンの定期実施

ひとつひとつは難しくありませんが、「毎回やる」ことが何より大切です。
次回予告

次回は、Webセキュリティの古典にして現役の脅威、
「SQLインジェクションの防止:システムセキュリティを守るための完全ガイド」
をお届けする予定です。どうぞお楽しみに!