質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
HTML5

HTML5 (Hyper Text Markup Language、バージョン 5)は、マークアップ言語であるHTMLの第5版です。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

XSS

XSS【クロスサイトスクリプティング】は、 ソフトウェアのセキュリティホールの一つで、Webサイトに脆弱性が あることからその脆弱性を利用し攻撃する手法です。 主に、入力フォームなどから悪意あるスクリプトを挿入し 該当ページを閲覧したブラウザ上でそのスクリプトを実行します。

Q&A

2回答

1723閲覧

クライアント側でユーザーの入力した値を HTML として表示する場合の XSS の可能性について

dwayne_johnson

総合スコア86

HTML5

HTML5 (Hyper Text Markup Language、バージョン 5)は、マークアップ言語であるHTMLの第5版です。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

XSS

XSS【クロスサイトスクリプティング】は、 ソフトウェアのセキュリティホールの一つで、Webサイトに脆弱性が あることからその脆弱性を利用し攻撃する手法です。 主に、入力フォームなどから悪意あるスクリプトを挿入し 該当ページを閲覧したブラウザ上でそのスクリプトを実行します。

1グッド

8クリップ

投稿2022/08/28 05:21

編集2022/08/28 05:27

ユーザーの入力した HTML 文字列を HTML としてサイトに表示したい

ユーザーが入力した HTML 文字列を、プレビュー欄的な箇所に HTML として解析して表示することを考えています。

その文字列をサーバーに保存などは行わず、クライアント側の JavaScript 内で処理は完了します。
プレビュー用の文字列を URL パラメータに保存するなども行わないです。(他のユーザーに任意の文字列を表示させることはできない)

この場合、いわゆる XSS の危険性として考えられるのは

  • その入力した HTML に悪意ある script が含まれていて、そのドメイン上から送信されたリクエストとして処理されてしまう危険性がある
    • Same Origin Policy が通ってしまい、ユーザーが勝手に作ったリクエストを送信することが可能になる?

ことだけかなと思っているのですが、セキュリティ周りに疎くこの認識があっているか心配です。

また、対策としては HTML を sanitize する処理をクライアント側に挟んで、script タグや onclick などのイベントハンドラーを許可しない(取り除く)ようにすることで十分かと考えているのですが、合ってますでしょうか?

趣味の開発で特に公開するサイトではないですが、せっかくなので気をつけておきたく、よろしくお願いします。

glyzinieh👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

qwrytyytyt

2022/08/28 06:06

許容するHTML(<b>や<font>)のみを表示するような仕組みを考えたほうがいいのでは?
guest

回答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
ockeghem

総合スコア11701

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

XSS対策

また、対策としては HTML を sanitize する処理をクライアント側に挟んで、script タグや onclick などのイベントハンドラーを許可しない(取り除く)ようにすることで十分かと考えているのですが、合ってますでしょうか?

JavaScriptにおけるXSS対策は、以下の2点だけ気を付ければよいと思います。

(対策1) HTML断片/XML断片を挿入するAPIを使わない

HTML断片/XML断片をドキュメントに挿入するAPIはXSSを引き起こす可能性がある為、使用してはいけません。

(対策2) DOMノードを挿入するAPIを使う

「要素ノード」「テキストノード」など、DOMノードを生成して、Node#appendChild()で生成したノードをドキュメントに挿入してください。

ただし、「属性名」や「属性値」にも安全でないものが含まれる為、これらを生成する際にはホワイトリストで対処する必要があります。

  • 要素生成時にホワイトリスト内の要素名のみを生成する(例: 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!&lt;p class = "foo"&gt;foo&lt;/p&gt;bar</textarea> 5<div class="output"></div> 6 7<textarea class="input">&lt;p class = "foo"&gt;foo&lt;/p&gt;bar</textarea> 8<div class="output"></div> 9 10<textarea class="input">&lt;p class = "foo" &gt;foo&lt;/p&gt;bar</textarea> 11<div class="output"></div> 12 13<textarea class="input">&lt;p id="foo"&gt;foo&lt;/p&gt;</textarea> 14<div class="output"></div> 15 16<textarea class="input">&lt;div class = "foo" &gt;foo&lt;/div&gt;</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

think49

総合スコア18162

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問