MarkdownをHTMLへ変換する方法とXSS対策:PHP・JavaScript・Vue.jsの実装例
作成日:2026.08.12
PHP のleague/commonmarkと、Node.js のMarked・DOMPurifyを使い、MarkdownをHTMLへ変換する方法を紹介します。ユーザー入力やAI生成Markdownを未信頼入力として扱い、Vue.js のv-htmlへ安全に表示するための設定と注意点も整理します。
目次
Markdown形式で文章を保存しておき、必要なときにHTMLへ変換して表示すたいケースがあります。
ただし、Markdownがユーザー入力やAI生成された文章の場合、変換したHTMLをそのまま表示してよいとは限りません。Markdownの中にHTMLタグや危険なURLが含まれている可能性があるためです。
今回は、PHP のleague/commonmarkと、Node.js のMarked・DOMPurifyを使って、MarkdownをHTMLへ変換する方法を比較します。Vue.js で表示する場合のXSS対策や、AI生成Markdownを扱うときの注意点も確認します。
前提環境
今回の前提環境は以下の通りです。
- PHP 8.3.15
- Node.js 20.20.2
- Vue.js 3.5.39
- Composer
- npm
ライブラリのバージョンは、インストール時期や他の依存関係によって変わります。実際に記事のコードを試すときは、composer.lockやnpmのロックファイルも保存し、使用したパッケージのバージョンを確認してください。
MarkdownをHTMLへ変換するときの考え方
MarkdownをHTMLへ変換する処理は、次のような流れになります。
Markdown
↓ Markdownパーサー
HTML
↓ 未信頼の入力であればサニタイズ
表示に使うHTML
Markdownパーサーは、見出しやリストなどの記法をHTMLへ変換します。しかし、変換したHTMLがアプリケーションにとって安全かどうかは、別途考える必要があります。
例えば、次のような入力を考えます。
# お知らせ
<img src=x onerror="alert('XSS')">
[危険なリンク](javascript:alert('XSS'))
このようなHTMLタグやURLを含むMarkdownを、変換後のHTMLとしてinnerHTMLやVueのv-htmlへ直接渡すのは危険です。
ユーザーが入力したMarkdownだけでなく、AIが生成したMarkdownも同じように未信頼の入力として扱います。AIが生成した文章だから安全、という前提でHTMLへ変換しないようにします。
PHPでMarkdownをHTMLへ変換する
league/commonmarkを導入する
PHPでは、league/commonmarkをComposerで導入します。
composer require league/commonmark
composer show league/commonmark
league/commonmarkはCommonMarkやGitHub Flavored Markdownに対応したPHP用のMarkdownパーサーです。詳しくはleague/commonmarkのインストール方法を参照してください。
基本的な変換処理
まず、MarkdownをHTMLへ変換するだけの最小例を作成します。
<?php
declare(strict_types=1);
use League\CommonMark\CommonMarkConverter;
require __DIR__ . '/vendor/autoload.php';
$converter = new CommonMarkConverter();
$markdown = <<<'MARKDOWN'
# Markdownの見出し
これは**太字**を含む文章です。
- リスト1
- リスト2
MARKDOWN;
echo $converter->convert($markdown)->getContent();
CommonMarkConverterを作成し、convert()へMarkdownを渡します。変換結果はレンダリング済みのコンテンツとして返るため、getContent()でHTML文字列を取り出します。
未信頼のMarkdownを変換する
ユーザー入力やAI生成Markdownを扱う場合は、セキュリティに関する設定を指定します。
<?php
declare(strict_types=1);
use League\CommonMark\CommonMarkConverter;
require __DIR__ . '/vendor/autoload.php';
$converter = new CommonMarkConverter([
// Markdown内のraw HTMLをHTMLとして解釈せず、エスケープする
'html_input' => 'escape',
// javascript:、vbscript:、file:などの危険なリンクを許可しない
'allow_unsafe_links' => false,
// 複雑すぎる入力による処理時間の増大を抑えるための上限例
'max_nesting_level' => 50,
'max_delimiters_per_line' => 100,
]);
$input = $_POST['markdown'] ?? '';
$markdown = is_string($input) ? $input : '';
echo $converter->convert($markdown)->getContent();
html_inputをescapeにすると、Markdown内に書かれたraw HTMLをHTML要素として出力せず、文字列として表示できます。raw HTMLを完全に取り除きたい場合は、要件に応じてstripを検討します。
allow_unsafe_linksをfalseにすると、javascript:などの危険なプロトコルを使ったリンクや画像を許可しない設定になります。max_nesting_levelとmax_delimiters_per_lineは、深すぎる構造や複雑すぎる入力による処理時間の増大を抑えるための上限例です。
これらの設定値は、すべてのアプリケーションでそのまま使える正解ではありません。許可するMarkdown記法、文章の長さ、表示するHTML要素を確認した上で決めます。詳細はleague/commonmarkのセキュリティ設定と設定項目の公式ドキュメントを参照してください。
例えば、raw HTMLを許可したい場合は、Markdownパーサーの設定だけで判断を終わらせず、変換後のHTMLに対して別の許可リスト型サニタイザーを使う方法もあります。その場合は、画像、リンク、コードブロックなどが意図せず壊れないかを含めて検証が必要です。
入力サイズとアプリケーション側の確認
Markdownの変換処理だけでなく、アプリケーション側でも入力サイズや権限を確認します。
$input = $_POST['markdown'] ?? '';
if (!is_string($input)) {
throw new InvalidArgumentException('Markdown must be a string.');
}
if (strlen($input) > 100_000) {
throw new InvalidArgumentException('Markdown is too large.');
}
$html = $converter->convert($input)->getContent();
実際のフォームでは、CSRF対策、ログインユーザーの権限確認、エラーメッセージの扱いなども必要です。Markdownを変換できることと、誰でも任意の文章を保存できることは別の問題として考えます。
JavaScriptでMarkdownをHTMLへ変換する
MarkedとDOMPurifyを導入する
Node.jsやVue.jsのプロジェクトでは、npmでmarkedとdompurifyを導入します。
npm install marked dompurify
npm list marked dompurify
MarkedはMarkdownをHTMLへ変換するライブラリです。ただし、Markedの公式ドキュメントにも、出力HTMLをサニタイズしないため、未信頼の入力を扱う場合は別途フィルタリングが必要だと記載されています。
変換後にDOMPurifyでサニタイズする
ブラウザで表示するHTMLを作る場合は、Markedの出力をDOMPurifyへ渡します。
import { marked } from 'marked';
import DOMPurify from 'dompurify';
export function markdownToSafeHtml(markdown) {
const dirtyHtml = marked.parse(markdown);
return DOMPurify.sanitize(dirtyHtml, {
USE_PROFILES: {
html: true,
},
});
}
ここでは、Markdownを変換したHTMLをdirtyHtmlとして扱い、DOMPurifyでサニタイズしてから返しています。USE_PROFILES: { html: true }は、HTML用途に対象を絞る設定例です。許可する要素や属性を変更する場合は、必要なものだけを明示する方針にします。
DOMPurifyの使い方や設定については、DOMPurifyの公式リポジトリを参照してください。サニタイズした後に別のライブラリでHTMLを書き換えると、安全性が損なわれる可能性があるため、サニタイズ後のHTMLを加工しないことも重要です。
Vue.jsで表示する
Vue.js 3.5.39で、入力したMarkdownをプレビュー表示する例です。
<script setup>
import { computed, ref } from 'vue';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
const markdown = ref('# Markdownのプレビュー\n\n**太字**で表示します。');
const safeHtml = computed(() => {
const dirtyHtml = marked.parse(markdown.value);
return DOMPurify.sanitize(dirtyHtml, {
USE_PROFILES: {
html: true,
},
});
});
</script>
<template>
<textarea v-model="markdown" rows="10" cols="60"></textarea>
<article v-html="safeHtml"></article>
</template>
テンプレートではsafeHtmlをv-htmlへ渡しています。v-htmlは文字列をHTMLとして解釈するため、ここへ渡す値はサニタイズ済みのHTMLだけに限定します。
Vueの通常のテキストバインディングは、HTMLをエスケープして表示します。一方、v-htmlやinnerHTMLはHTMLを解釈するため、ユーザー入力をそのまま渡さないでください。詳しくはVue.jsのセキュリティガイドを参照してください。
ユーザー入力とAI生成Markdownを扱う
AI生成だから安全とは限らない
AIに「Markdownで記事を書いてください」と依頼すると、見出しやリストだけでなく、HTMLタグやリンクを含む文章が返ることがあります。AIの出力は、アプリケーションが管理している固定コンテンツとは限りません。
AI生成Markdownを保存・表示する場合も、次のような入力として扱います。
- raw HTMLを含む可能性がある入力
- 安全でないURLを含む可能性がある入力
- 想定外に長い、または複雑な入力
- 表示先のアプリケーションで意味が変わる入力
特に、AIの出力に含まれるリンクや画像は、XSSだけでなく、意図しない外部サイトへの遷移や外部リソースの読み込みにつながる場合があります。HTMLとして表示する前に、アプリケーションの要件に合うか確認します。
保存時と表示時のどちらで処理するか
Markdownを扱う方法には、主に次の2つがあります。
| 方法 | 特徴 | 注意点 |
|---|---|---|
| 保存時にHTML化・サニタイズ | 表示時の処理を減らしやすい | 後からセキュリティ方針を変更したときに、既存データの再処理が必要 |
| 表示時にHTML化・サニタイズ | 現在のライブラリや表示方針を反映しやすい | 表示のたびに変換処理が必要。キャッシュする場合は更新管理が必要 |
編集機能がある場合は、元のMarkdownを保持しておき、表示用HTMLを派生データとして扱う方法が分かりやすいかと思います。サニタイズ済みHTMLだけを元データにすると、後からMarkdownとして編集しにくくなります。
サーバー側でも表示する場合は、クライアント側のJavaScriptだけを安全性の基準にしないようにします。APIやデータベースへ保存する前に、入力サイズ、リンクの形式、利用者の権限などをサーバー側で確認し、サーバーからHTMLを返す場合はサーバー側でも適切に変換・サニタイズします。
サニタイズ後のHTMLを変更しない
DOMPurifyでサニタイズした後に、別の処理でHTML文字列を連結したり、属性を追加したりすると、安全性の前提が崩れる可能性があります。
表示用HTMLへクラスやリンク属性を追加したい場合は、Markdown変換前の構造や、サニタイズ処理の設定で対応できるかを確認します。サニタイズ後の文字列へ、ユーザー入力を使ってHTMLを追加する処理は避けます。
PHPとJavaScriptの使い分け
| PHP + league/commonmark | JavaScript + Marked + DOMPurify | |
|---|---|---|
| 変換場所 | サーバー側 | ブラウザ側、またはDOM環境を用意したNode.js側 |
| 向いている場面 | サーバーから完成したHTMLを返したい場合 | 入力中のプレビューなど、画面上で即時変換したい場合 |
| 安全性の要点 | raw HTML、危険なリンク、入力の複雑さを設定で制限する | Markedの出力をDOMPurifyでサニタイズしてから表示する |
| 注意点 | HTMLを許可する場合は、別のサニタイズ方針も検討する | サニタイズ前の値をv-htmlやinnerHTMLへ渡さない |
サーバー側でHTMLを生成する必要があるならPHP側で処理し、入力中のプレビューなどブラウザ上での反映が必要ならJavaScript側で処理する、と考えると選びやすくなります。
どちらを選ぶ場合でも、Markdownの生成元を信頼してよいか、HTMLをどこへ出力するか、どの記法を許可するかを先に決めることが重要です。
動作確認で確認すること
まず、指定した実行環境とパッケージのバージョンを確認します。
php -v
node --version
composer show league/commonmark
npm list marked dompurify
次に、通常のMarkdownが意図したHTMLになるかを確認します。
- 見出し、段落、強調、リスト
- リンク、画像、コードブロック
- 日本語、改行、長い文章
その後、次のようなセキュリティ検証用の入力を、開発環境で確認します。
<script>alert('XSS')</script>
<img src=x onerror="alert('XSS')">
[危険なリンク](javascript:alert('XSS'))
<svg onload="alert('XSS')"></svg>
確認するのは、アラートが表示されないことだけではありません。変換後のHTMLに危険な要素や属性が残っていないこと、必要な見出しやコードブロックまで意図せず削除されていないことも確認します。
セキュリティ検証用の入力を、本番環境や実際の利用者が閲覧する場所へ保存しないように注意してください。
まとめ
今回は、PHPのleague/commonmarkと、JavaScriptのMarked・DOMPurifyを使ってMarkdownをHTMLへ変換する方法を整理しました。
サーバー側で変換する場合は、league/commonmarkのhtml_inputやallow_unsafe_linksなどを確認し、未信頼の入力に対する設定を行います。ブラウザ側で変換する場合は、MarkedがHTMLをサニタイズしないため、DOMPurifyで処理してからv-htmlやinnerHTMLへ渡します。
ユーザー入力やAI生成Markdownは、生成元だけを理由に信頼してはいけません。Markdownを保存するのか、どのHTML記法を許可するのか、どのタイミングでサニタイズするのかを決めた上で、実際の表示先に合わせて実装するとよいかと思います。
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
問い合わせフォームにGoogle reCAPTCHA v3を組み込む方法
2023.04.07
スパムボットによる大量の問い合わせを防ぐために、GoogleのreCAPTCHA v3を導入する方法を解説します。reCAPTCHAを利用することで、フォーム送信時に自動的にボットを検出し、不審な送信を防ぐことができます。サイトの登録から、HTMLコードの埋め込み、サーバーサイドでの照合処理まで、必要な手順を順を追って説明します。
URLエンコード・デコードの実装方法(Perl、PHP、JavaScript対応)
2009.12.02
URLに2バイト文字を含めるためのエンコード・デコード方法を、Perl、PHP、JavaScriptの3言語でまとめました。各言語のシンプルな関数を使った実装例を紹介し、文字列を正しく処理する方法を解説します。
Windows 11でLaravel 13を始める:環境構築からルーティング・Controller・Bladeまで
2026.08.13
Windows 11にPHP、Composer、Laravel Installerを用意し、Laravel 13のプロジェクトを作成します。routes/web.php、Controller、Bladeの役割を説明し、最小構成のWebページを表示するまでの流れと、Windows環境でつまずきやすい点を紹介します。
PHPでTOTP方式のMFAを実装する|Imagick不要のSVG QRコード生成
2026.08.08
PHP 8.3.15の環境で、認証アプリを使ったTOTP方式のMFAを実装します。spomky-labs/otphpでワンタイムコードを検証し、bacon/bacon-qr-codeでImagickを使わずにSVG形式のQRコードを生成します。MFAやTOTPの基本概念から、登録処理、簡略化したログイン処理、本番環境での注意点まで紹介します。