marked.js便利ですよね
Markdownをプレビューしたり、HTMLで表示させる時にクライアント側で変換してもらうためにmarked.jsを使う事があると思います。
ですが、うっかりしてると結構致命的なミスを引き起こしてしまいます。
##前提
marked|githubがすでに使える状態とします。
STEP1 sanitize-html等のライブラリを入れる
Marked自身にもサニタイズオプションはありますが、現在それの使用は非推奨です。
Warning: 🚨 Marked does not sanitize the output HTML. Please use a sanitize library, like DOMPurify (recommended), sanitize-html or insane on the output HTML! 🚨
環境にあったライブラリを選定して入れましょう。
私はsanitize-htmlを選びました。
#STEP2 サニタイズする
markedはmarked(text)
という形でtextをHTMLに変換した値を返してくれます。
sanitize-htmlはsanitizeHtml(text)
とするとtextから<script></script>
等を削除して返します。
簡単ですね。
では、あなたはこれら2つを使ってプレビュー付きのMarkdownエディタを作る時、この2つの関数をどのようにつなげますか?
(プレビューするだけならサニタイズの必要性は薄いのですが、一般に公開するときも同様の手順でサニタイズするとします)
フォームはこんな感じとします
<template>
...
<textarea
id="editor-content"
:value="textareaValue"
name="editor-textarea-content"
@input="convertMarkdown"
></textarea>
<div id="editor-preview" v-html="convertHtml"></div>
...
</template>
<script>
const marked = require('marked')
const sanitizeHtml = require('sanitize-html')
const _ = require('lodash')
export default {
head() {
return {
title: 'テストサイト'
}
},
components: {
headerComponent
},
data() {
return {
textareaValue: ''
}
},
computed: {
convertHtml() {
//ここでmarkdownの変換と、エスケープ処理を行う
}
},
mounted() {
marked.setOptions({
breaks: true
})
},
methods: {
convertMarkdown: _.throttle(function(e) {
console.log(e.target.value)
this.textareaValue = e.target.value
}, 500)
}
}
</script>
例がNuxtなので念の為解説すると、textareaに何か入力されたらconvertMarkdownが値を保存し、convertHtmlがそれを変換するという形です。、下のdivに内容がHTML化されたものが表示されるという感じで、他の部分は値の受け渡し等の設定と思ってください。
追記: 実用的にするため、const _ = require('lodash')
を用いて、inputで関数が呼ばれすぎない様にしました。
ここでの書き方は大まかに2つあります。
methods: {
convertMarkdown() {
const conbertContent = marked(this.textareaValue)
this.convertHtml = sanitizeHtml(conbertContent)
}
}
と
methods: {
convertMarkdown() {
const conbertContent = sanitizeHtml(this.textareaValue)
this.convertHtml = marked(conbertContent)
}
}
です。どちらもサニタイズ関数を用いており一見問題ないのですが、これ1つは簡単にJavaScriptを実行できてしまうんです。
欠陥があるのは、先にサニタイズしている2番目の例です。
どちらもtextareaに<script>alert(1);</script>
なんかを入れてもきっちり弾くので「よし、問題ないな」と思ってしまう事もあると思うんですが、
後者だとbugbounty-cheatsheetのMarkdwodn xss例の
[a](javascript:window.onerror=confirm;throw%201)
の様なmarkedで展開されるリンクまでは完璧にサニタイズしてくれないので、ポップアップが出てきてしまいます。
まとめ
Markdownのサニタイズは一番最後!
(ちなみにh1やh2はサニタイズで弾かれるので、適時許可しましょう)