【実務・中級編】MarkdownパーサーにおけるXSS脆弱性とサニタイズ – アプリケーションセキュリティ & 安全な開発防御ガイド

Markdownの「その変換」、実は爆弾を抱えていないか?:DOMPurifyによる防御の極意

現場でエンジニア諸君と話していると、よくこんな相談を受ける。「MarkdownをHTMLに変換して表示するだけの機能なんだから、そこまで神経質にならなくてもいいだろう?」と。

残念ながら、その認識こそが攻撃者にとっての「招待状」だ。Markdownパーサーは非常に便利だが、その多くは「HTMLの直接注入」を許容する設計になっている。MarkdownにHTMLタグを埋め込める仕様は、諸刃の剣どころか、脆弱性の温床そのものだ。

今日は、Markdownを安全に扱うための「泥臭い現実」と、その防御パイプラインについて解説する。

—

なぜMarkdownパーサーは「毒」を吐くのか

一般的なMarkdownパーサー(markedやmarkdown-itなど)は、入力された文字列を解析し、HTML文字列を生成する。この際、攻撃者は以下のような「毒入り」のMarkdownを投げ込む。

脆弱な投稿の例
[Click me](javascript:alert(document.cookie))

もしパーサーの設定がデフォルトのままであれば、これらはそのままHTMLとしてDOMに挿入される。ブラウザはこれを「正しいHTML」と解釈し、悪意あるスクリプトを実行してしまう。これがXSS(クロスサイトスクリプティング)の入り口だ。

「サニタイズは出力時にやればいい」? 正直、甘い。「変換と同時に無害化」するのが、インシデントを未然に防ぐプロの作法だ。

—

推奨される防御パイプライン:DOMPurifyの導入

現代のWeb開発において、フロントエンドでMarkdownを表示するなら、DOMPurify を使わない理由は存在しない。DOMPurifyは、DOMベースのXSS攻撃を専門に防ぐための強力なライブラリだ。

実装サンプル:JavaScript (React/Vue/Vanilla)

以下のコードは、Markdownを変換した直後にDOMPurifyで「焼き払う」処理だ。

import { marked } from ‘marked’;
import DOMPurify from ‘dompurify’;

/

  • 安全にMarkdownをHTML文字列に変換する関数
  • @param {string} rawMarkdown – ユーザー入力のMarkdown
  • @returns {string} – サニタイズ済みのHTML

/
function secureMarkdownToHtml(rawMarkdown) {
// 1. まずMarkdownをHTMLに変換
const dirtyHtml = marked.parse(rawMarkdown);

// 2. DOMPurifyで不要なタグや属性を徹底的に除去
// RETURN_DOM: true を使うことで、Nodeオブジェクトとして操作することも可能
return DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: [‘b’, ‘i’, ‘em’, ‘strong’, ‘a’, ‘p’, ‘ul’, ‘ol’, ‘li’, ‘code’, ‘pre’],
ALLOWED_ATTR: [‘href’, ‘title’],
// javascript: スキーム等を強制的にブロックするための設定
FORBID_TAGS: [‘script’, ‘style’, ‘iframe’, ‘object’, ‘embed’],
});
}

この実装の肝は、ALLOWED_TAGS をホワイトリスト形式で管理している点だ。「許可したもの以外はすべて捨てる」というこの姿勢が、未知の攻撃手法に対する最大の防壁になる。

—

バックエンドでの対策(Python/Flask等の場合)

もしサーバーサイドでMarkdownをHTMLに変換してDBに保存しているのなら、そのアーキテクチャ自体を見直す必要がある。DBには「生のMarkdown」を保存し、表示する直前(クライアント側)でサニタイズするのが現代的な正解だ。

どうしてもサーバー側で処理しなければならない場合は、Pythonのbleachライブラリを使うのが定石だ。

import bleach
import markdown

def secure_process(raw_markdown):
# Markdown変換
html = markdown.markdown(raw_markdown)

# サニタイズ(ホワイトリスト形式)
allowed_tags = [‘p’, ‘b’, ‘i’, ‘strong’, ‘em’, ‘a’]
allowed_attrs = {‘a’: [‘href’, ‘title’]}

return bleach.clean(html, tags=allowed_tags, attributes=allowed_attrs)

—

セキュリティチーフからの「盲点」への助言

コードが書けたら終わりではない。以下の設定も必ず確認してほしい。

1. Content Security Policy (CSP) の併用:
仮にサニタイズをすり抜ける攻撃があったとしても、CSPで防ぐ。「防御の多層化」だ。
Content-Security-Policy: default-src 'self'; script-src 'self';
これだけで、外部ドメインからのスクリプト実行やインラインスクリプトを遮断できる。

2. パーサーのオプション設定:
markdown-it等を使う場合、html: falseというオプションがある。これを有効にすると、そもそもMarkdown内のHTMLタグを無視する。これが最強の対策だ。要件にHTML埋め込みが不要なら、このフラグを立てるだけで脆弱性はゼロになる。

最後に:エンジニアとしての矜持

セキュリティは「ツールを入れたら終わり」の儀式ではない。「ユーザーは何を投げ込んでくるか分からない」という性悪説を前提に、データがどこを通り、どうブラウザに到達するのかを想像する力だ。

今回の実装をベースに、君たちのプロジェクトがより強固なものになることを期待している。もし、「うちのシステムは古くてDOMPurifyが入れられない」といった泥臭い課題があれば、いつでも相談してくれ。一緒に解決策を導き出そう。

健闘を祈る。

コメント

タイトルとURLをコピーしました