ディレクトリ一覧での格納型クロスサイトスクリプティング

ディレクトリ一覧での格納型クロスサイトスクリプティング

説明

ファイル名や投稿タイトルなどの保存された値を、適切な出力エンコードなしでHTMLとして表示すると、格納型XSSが発生します。攻撃者が保存したマークアップが、後からページを開いたユーザーのブラウザーで実行されるおそれがあります。例えば、アップロードされたファイルの名前をディレクトリ一覧のHTMLへ直接挿入すると、悪意のある内容が動作する可能性があります。

想定される影響

  • スクリプトが読み取れるCookieやトークンを盗まれ、ログインセッションを乗っ取られるおそれがあります。
  • ユーザーや管理者の権限を悪用される可能性があります。
  • 画面の改ざんや偽の入力フォームで機密情報を収集されるおそれがあります。
  • ブラウザーの権限を利用して内部APIを呼び出したり、状態を変更するリクエストを送られたりする可能性があります。
  • 保存された内容を除去するまで、多数のユーザーに繰り返し影響が及ぶおそれがあります。

対処方法

  • 保存された値をHTML本文、属性、URLへ挿入する前に、出力先の文脈に合わせてエンコードしてください。
  • EJS、Pug、Nunjucksなどで出力エスケープを有効にし、信頼できない値をHTMLへ直接連結しないでください。
  • 可能ならHTMLを組み立てず、res.json でデータを返してください。
  • ファイル名などの保存する入力は、用途に適した許可文字を使って検証・正規化してください。
  • CSP、X-Content-Type-Options、適切な HttpOnly・Secure・SameSite Cookie属性で被害を抑えてください。
  • エンコードや検証には escape-html、he、用途に合う validator の関数など、実績のあるライブラリを使ってください。

例

変更前

javascript
const express = require("express");
const fs = require("fs");
const app = express();

// 保存されたファイル名をHTMLへ直接挿入
app.get("/files", (req, res) => {
  fs.readdir("/var/app/uploads", (err, names) => {
    if (err) return res.status(500).send("error");
    const body =
      "<h2>Files</h2><ol>" +
      names.map((n) => "<li>" + n + "</li>").join("") +
      "</ol>";
    // BAD: n内の<script>などのタグが実行される可能性がある
    res.send(body);
  });
});

app.listen(3000);

変更後

javascript
const express = require("express");
const fs = require("fs");
const escapeHtml = require("escape-html"); // he、validatorなども選択肢
const app = express();

// HTMLの出力文脈に合わせてエンコード
app.get("/files", (req, res) => {
  fs.readdir("/var/app/uploads", (err, names) => {
    if (err) return res.status(500).send("error");
    const safeItems = names
      .map((n) => "<li>" + escapeHtml(n) + "</li>")
      .join("");
    const body = "<h2>Files</h2><ol>" + safeItems + "</ol>";
    res.set("Content-Type", "text/html; charset=utf-8");
    res.send(body);
  });
});

// 別の方法: JSONを返し、クライアントの安全なテキストAPIで表示
app.get("/files.json", (req, res) => {
  fs.readdir("/var/app/uploads", (err, names) => {
    if (err) return res.status(500).json({ error: "error" });
    res.json({ files: names });
  });
});

app.listen(3000);

解説:

  • 変更前: fs.readdir で取得した信頼できないファイル名をHTMLへ連結し、res.send で返しています。悪意のあるHTMLを含む名前が保存されると、一覧を開いたユーザーに格納型XSSの被害が及ぶ可能性があります。
  • 変更後: escape-html で各ファイル名をエンコードし、タグではなくテキストとして表示します。JSONを返す方法でも、クライアント側で値をHTMLとして再解釈しないようにしてください。

参考資料