ユーザーの入力した HTML 文字列を HTML としてサイトに表示したい
ユーザーが入力した HTML 文字列を、プレビュー欄的な箇所に HTML として解析して表示することを考えています。
その文字列をサーバーに保存などは行わず、クライアント側の JavaScript 内で処理は完了します。
プレビュー用の文字列を URL パラメータに保存するなども行わないです。(他のユーザーに任意の文字列を表示させることはできない)
この場合、いわゆる XSS の危険性として考えられるのは
- その入力した HTML に悪意ある script が含まれていて、そのドメイン上から送信されたリクエストとして処理されてしまう危険性がある
- Same Origin Policy が通ってしまい、ユーザーが勝手に作ったリクエストを送信することが可能になる?
ことだけかなと思っているのですが、セキュリティ周りに疎くこの認識があっているか心配です。
また、対策としては HTML を sanitize する処理をクライアント側に挟んで、script
タグや onclick
などのイベントハンドラーを許可しない(取り除く)ようにすることで十分かと考えているのですが、合ってますでしょうか?
趣味の開発で特に公開するサイトではないですが、せっかくなので気をつけておきたく、よろしくお願いします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答2件
0
ご懸念の内容は「セルフXSS」という問題です。詳しくは以下のQ&Aを参照ください。
不特定多数がページ上で任意のJavascript等を実行できるページは危険でしょうか?
(以下、過去の回答はピント外れだったので書き直しました)
対策ですが、「何もしない」という選択肢はありますが、緩和策として以下の3種類を単独、あるいは組み合わせて使用することができます。
(1) サニタイズ
これは質問者さんも言及されていますが、ユーザが指定したHTMLがイベントハンドラなど危険な属性を取り除くものです。script要素については、innerHTMLを使って表示する場合はJavaScriptが実行されませんが、逆に言うと存在しても「何もしない」ので除去するのがよいでしょう。
サニタイズの実装についてはthink49さんからも提案がありますが、いささか面倒な処理であるので既存のライブラリを使うのが良いでしょう。
たとえば、アプリケーションをAnguarで開発する案が考えられます。Angularは元々サニタイズの処理が内蔵されていますので、以下のようにHTML文字列を表示する処理を書いたとしますと、
HTML
1<span [innerHTML]="htmlString"></span>
これだけで危険な属性などは除去してくれます。たとえば、htmlStringに以下の文字列を指定すると、
<img src=1 onerror=alert(1)>
生成されるDOMは以下となります。危険なonerror属性が除去されています。
HTML
1<img src=1>
他のフレームワークでもサニタイズのライブラリを追加することはできるでしようが、Angularであればサニタイズ機能が元々内蔵されているので簡便です。
(2) コンテンツセキュリティポリシー
コンテンツセキュリティポリシー(CSP)はHTTPレスポンスヘッダに指定することで、仮にXSS脆弱性があっても、JavaScriptの実行を止めてくれます。
例えば、HTTPレスポンスヘッダに以下のように指定します。
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none';
CSPを使うとJavaScriptをHTML内に書くことに制約が出てきますが、Angularなどで開発するとビルド時にHTMLとJavaScriptを分離してくれるので問題が出にくくなります。一方、jQueryなどで開発する場合はCSPを設定するのは面倒です。
(3) サンドボックスドメイン
HTMLを表示するサイトのドメインをアプリケーションとは別のものにする方法です。別のドメインをサンドボックスドメインと呼びます。サンドボックス側でJavaScript等が実行されても、アプリケーション側には影響しないとう性質を利用するものです。この方法は、手法として知っておいて損のないものですが、今回利用するにはやや大掛かり過ぎるかと思います。
投稿2022/08/28 06:19
編集2022/08/29 23:34総合スコア11705
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
XSS対策
また、対策としては HTML を sanitize する処理をクライアント側に挟んで、script タグや onclick などのイベントハンドラーを許可しない(取り除く)ようにすることで十分かと考えているのですが、合ってますでしょうか?
JavaScriptにおけるXSS対策は、以下の2点だけ気を付ければよいと思います。
(対策1) HTML断片/XML断片を挿入するAPIを使わない
HTML断片/XML断片をドキュメントに挿入するAPIはXSSを引き起こす可能性がある為、使用してはいけません。
- element.insertAdjacentHTML - Web API | MDN
- Element.innerHTML - Web API | MDN
- Element.outerHTML - Web API | MDN
- Range.createContextualFragment() - Web APIs | MDN
- Range.createContextualFragment() - Web APIs | MDN
- DOMParser - Web API | MDN
(対策2) DOMノードを挿入するAPIを使う
「要素ノード」「テキストノード」など、DOMノードを生成して、Node#appendChild()で生成したノードをドキュメントに挿入してください。
- Document.createDocumentFragment() - Web API | MDN
- Document.createElement() - Web API | MDN
- Document.createTextNode() - Web API | MDN
ただし、「属性名」や「属性値」にも安全でないものが含まれる為、これらを生成する際にはホワイトリストで対処する必要があります。
- 要素生成時にホワイトリスト内の要素名のみを生成する(例: script要素は安全ではない為、ホワイトリストに含めない)
- 属性生成時にホワイトリスト内の属性名のみを生成する(例: onclick属性は安全ではない為、ホワイトリストに含めない)
- 属性生成時にホワイトリスト内の属性値のみを生成する(例: href属性においては、http,httpsスキームのみを許可する)
実装
ユーザーの入力した HTML 文字列を HTML としてサイトに表示したい
実装手段としては、「ユーザが入力したHTML文字列」をparseして、ノード生成時にホワイトリストを通せば、XSS対策になります。
簡単な例として、次の条件を満たすコードを書いてみます。
<p></p>
を使える- class属性を使える(属性値はダブルコーテーションもしくはシングルコーテーションで括らなければならない。属性値内の
<>"'
はエスケープしなければならない) - テキストノード値を使える(
<>
はエスケープしなければならない)
html
1<textarea class="input">Hello, World!</textarea> 2<div class="output"></div> 3 4<textarea class="input">Hello, World!<p class = "foo">foo</p>bar</textarea> 5<div class="output"></div> 6 7<textarea class="input"><p class = "foo">foo</p>bar</textarea> 8<div class="output"></div> 9 10<textarea class="input"><p class = "foo" >foo</p>bar</textarea> 11<div class="output"></div> 12 13<textarea class="input"><p id="foo">foo</p></textarea> 14<div class="output"></div> 15 16<textarea class="input"><div class = "foo" >foo</div></textarea> 17<div class="output"></div> 18 19<script> 20'use strict'; { 21 const listener = { 22 createElement: function createElement(doc, tagName) { 23 const tagNameList = new Set(['p']); // タグ名のホワイトリスト 24 25 if (!tagNameList.has(tagName)) throw new Error( 26 'DOMException: Failed to execute \'createElement\' on \'Document\': The tag name provided (\'' + 27 tagName + '\') is not a valid name.'); 28 29 return doc.createElement(tagName); 30 }, 31 setAttribute: function setAttribute(element, atName, atValue) { 32 const nameList = new Set(['class']); // 属性名のホワイトリスト 33 const valueList = new Map; // 属性値のホワイトリスト(属性名別に分割) ※class属性値はリスクゼロの為、ホワイトリストに含めない 34 35 if (!nameList.has(atName)) throw new Error( 36 'DOMException: Failed to execute \'setAttribute\' on \'Element\': \'' + atName + 37 '\' is not a valid attribute name.'); 38 if (valueList.has(atName) && !valueList.get(atName).test(atValue)) throw new Error( 39 'DOMException: Failed to execute \'setAttribute\' on \'Element\': \'' + atValue + 40 '\' is not a valid attribute value.'); 41 42 element.setAttribute(atName, atValue); 43 }, 44 handleEvent: function handleInput(event) { 45 const input = event.target, 46 doc = input.ownerDocument, 47 inputString = input.value; 48 const patternList = [ 49 '([^<>]+)', // テキストノードを表すHTML断片 50 '<(p)(?:\\s+class\\s*=\\s*("[^"<>]*"|\'[^\'<>]*\')\\s*)?>([^<>]*)</p>', // p要素ノードを表すHTML断片 51 '[\\s\\S]' // 上記以外の任意の不正文字(SyntaxError) 52 ]; 53 const reg = new RegExp(patternList.join('|'), 'gi'); 54 const df = doc.createDocumentFragment(); 55 let result; 56 57 while (result = reg.exec(inputString)) { 58 if (result[1]) { 59 df.appendChild(doc.createTextNode(result[1])); 60 } else if (result[2]) { 61 const p = df.appendChild(this.createElement(doc, 'p')); 62 63 const className = result[3]; 64 if (className) this.setAttribute(p, 'class', className.slice(1, -1)); 65 66 const textContent = result[4]; 67 if (textContent) p.textContent = textContent; 68 } else { 69 console.dir(result); 70 throw new SyntaxError('Unexpected token \'' + result[0] + '\''); 71 } 72 } 73 74 const output = input.nextElementSibling; 75 for (let childNode of output.childNodes) childNode.remove(); 76 output.appendChild(df); 77 } 78 }; 79 80 for (let input of document.getElementsByClassName('input')) { 81 input.addEventListener('input', listener, false); 82 input.dispatchEvent(new InputEvent('input')); 83 } 84} 85</script>
createElement(), setAttribute()
の処理でホワイトリストによるフィルタがかかる為、安全にノードを生成できています。
「<p id="foo">foo</p>」を入力した際のDOMExceptionエラーはパース処理をさぼってタグ名と属性を一括でパースしている為、<
でエラーが出る問題があります。
これは「タグ名のパース」と「属性名のパース」を個別に行えば、エラーが発生する文字の場所が正確になるよう修正できます。
HTML parser
前述のコードではparser処理を簡潔にする為、標準のHTMLにない文法上の制限を加えました。
- 属性値はダブルコーテーションもしくはシングルコーテーションで括らなければならない。属性値内の
<>"'
はエスケープしなければならない - テキストノード値において、
<>
はエスケープしなければならない
HTMLはルーズな言語なので、完全対応しようとすると、parserに入れる分岐処理が増えて、処理が複雑化してしまいます。
- テキスト上でエスケープされていない
<>
をテキストノードとして扱う - 属性値上でエスケープされていない
<>
を属性値として扱う
Webブラウザと同様にHTMLを扱うためにはHTML Standard既定のパーサの動きを忠実に再現する必要があります。
Re: dwayne_johnson さん
投稿2022/08/28 15:04
総合スコア18189
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。