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

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

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

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

Q&A

解決済

2回答

2102閲覧

ユーザーから受け取るhtml構造をホワイトリストを利用して無害化したい

pegy

総合スコア245

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

0グッド

0クリップ

投稿2021/04/23 02:09

編集2021/04/23 07:59

いつもお世話になっております。

前提とやりたいこと

ユーザーからhtml構造のデータをformで受け取る予定なのですが、html構造として出力させたいため、htmlspecialcharsを単純に利用できないという状況にございます。そこで、
0. srchrefといった不正なJSを埋め込まれる可能性がある属性については、Domを利用してチェック
0. タグについては、ホワイトリストを作成して限定して無害なものに置換→出力する際に一旦htmlspecialcharsして、ホワイトリスト対象の物だけ、戻すというアプローチ
で以下のコードを作成いたしました。

php

1$get_html = '<p>this is a test <a href="dammy">link</a><img src="dammy"></p>'; 2 3$dom = new DOMDocument(); 4$dom->loadHtml($get_html); 5$a_tag = $dom->getElementsByTagName('a'); 6$img_tag = $dom->getElementsByTagName('img'); 7 8foreach($a_tag as $attr) { 9 $item = $attr->getAttribute('href'); 10 $java = substr($item,0,4) == "java";//著しい不正処理 11 $http = substr($item,0,4) == "http"; 12 $https = substr($item,0,5) == "https"; 13 if ($java) {//著しい不正処理と認定して識別 14 } 15 if(!$http||!$https){ 16 $attr->setAttribute('href', '#'); 17 } 18} 19foreach($img_tag as $attr) { 20 $item = $attr->getAttribute('src'); 21 $java = substr($item,0,4) == "java";//著しい不正処理 22 $data = substr($item,0,4) == "data";//base 64形式で取得 23 24 if ($java) {//著しい不正処理と認定して識別 25 } 26 if(!$data){ 27 $attr->setAttribute('src', '#'); 28 } 29} 30$pre_html = $dom->saveHTML(); 31 32 $whiteList = [ 33 ["<p","*^[p"], 34 ["<span","*^[span"], 35 ["<strong","*^[strong"], 36 ["<em","*^[em"], 37 ["<u","*^[u"], 38 ["<s","*^[s"], 39 ["<blockquote","*^[blockquote"], 40 ["<ol","*^[ol"], 41 ["<ul","*^[ul"], 42 ["<li","*^[li"], 43 ["<sub","*^[sub"], 44 ["<sup","*^[sup"], 45 ["<a","*^[a"], 46 ["<iframe","*^[iframe"], 47 ["<img","*^[img"], 48 ]; 49 50 $whiteList_endTag = [ 51 ["<br>","[-/br-]"], 52 ["</p>","[/p]_*"], 53 ["</span>","[/span]_*"], 54 ["</strong>","[/strong]_*"], 55 ["</em>","[/em]_*"], 56 ["</u>","[/u]_*"], 57 ["</s>","[/s]_*"], 58 ["</blockquote>","[/blockquote]_*"], 59 ["</ol>","[/ol]_*"], 60 ["</ul>","[/ul]_*"], 61 ["</li>","[/li]_*"], 62 ["</sub>","[/sub]_*"], 63 ["</a>","[/a]_*"], 64 ["</iframe>","[/iframe]_*"], 65 ]; 66 67 $whiteList_search=[];$whiteList_replace=[]; 68 foreach ($whiteList as $value) { 69 $whiteList_search[]= $value[0]; 70 $whiteList_replace[]= $value[1]; 71 } 72 $whiteList_endTag_search=[];$whiteList_endTag_replace=[]; 73 foreach ($whiteList_endTag as $value) { 74 $whiteList_endTag_search[]= $value[0]; 75 $whiteList_endTag_replace[]= $value[1]; 76 } 77 78$pre_html = str_replace($whiteList_search,$whiteList_replace,$pre_html); 79$result_html = str_replace($whiteList_endTag_search,$whiteList_endTag_replace,$pre_html); 80 81echo $result_html; 82echo "<br>"; 83 84$rec_html = str_replace(["<html><body>","</body></html>"],["",""],$result_html ); 85$rec_html = preg_replace('@<!DOCTYPE.*>@',"",$rec_html); 86$rec_html = htmlspecialchars($rec_html, ENT_NOQUOTES, 'UTF-8'); 87$rec_html = str_replace($whiteList_replace,$whiteList_search,$rec_html); 88$rec_html = str_replace($whiteList_endTag_replace,$whiteList_endTag_search,$rec_html ); 89 90echo $rec_html;// ここです

問題点

ここで、特に$whiteListについてですが、"<p"という<+pにしており、"<p>""<p [regEx]>"のようにしていないかというと、属性値の種類や数はパターンがあり、"<p"と>の間の情報は残しつつタグを変換することはpreg_replace等ではできないという認識による物でした。

その結果として、htmlspecialcharsはタグ全体ではなく>も区別して識別することが判明し、$rec_html;// ここですにおいて、以下のような意図しない出力をもたらすことがわかりました。

html

1<p&gt;this is="" a="" test="" <a="" href="#" &gt;link<=""><img src="#" &gt;<="" p="">

ご質問

最終的には元の$get_html = '<p>this is a test <a href="dammy">link</a><img src="dammy"></p>';の構造に復帰させて、特定の属性値やホワイトリスト以外のタグを除去して出力させたいのですが、ここで行き詰まってしまいました。
例えば、strip_tags()Domdocument / SimplXMLなども検討したのですが、前者は基本的に使用すべきではないという記事を読んだり、後者は、調べても例えば、存在する要素ノード<p>を存在しない*^[]p^* などに置換する術を見つけることができませんでした。

この状況で、元のhtml構造に復帰させる術について、アドバイスを頂ければ幸いでございます。
よろしくお願い申し上げます。

加筆

yambejp様からのコメントも参考にdomを使用して、ホワイトリスト外のものをreplaceしてみるというアプローチを考えました。

例えば以下のようなreplaceChild() を使用したとしても、ノードはツリー構造を持っている(と推察)ため単純に含めたくないnodeNameやtagNameの要素をreplaceすることができません。。
やはり手詰まりです。

php

1$str = '<p>aaaa<span class="">bbb<table><tbody><td>a</td><tbody></table></span>a</p><img><script>alert(danger)</script>'; 2$dom = new DOMDocument(); 3$dom->loadHtml($str); 4$all_tag = $dom->getElementsByTagName('*'); 5$white_tag_list = ['p','span','strong','em','u','s','blockquote','ol','ul','li','sub','sup','a','iframe','img']; 6 7$replace_node =$dom->createElement('span'); 8 9 10foreach ($all_tag as $key) { 11 if (!in_array($key->nodeName,$white_tag_list,true)) { 12 // $key->tagName = "span"; 13 // $key->nodeName = "span"; 14 $key->parentNode->replaceChild($replace_node, $key); 15 var_dump($key); 16 } 17} 18 19echo $dom->saveHTML();

新たに試したこととして追記いたします。

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

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

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

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

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

guest

回答2

0

ベストアンサー

ユーザーからhtml構造のデータをformで受け取る予定なのですが

この要件は必須ですかね?

一番楽でかつ安全なのは、

  1. そもそもHTMLで受け取らずMarkdown,JSON,厳密なXMLやその他パースしやすい独自マークアップで受け取る
  2. 1で受け取ったマークアップをデータとしてパースしたものから、ホワイトリストに合致するものだけをHTMLとして組み立てていくというアプローチかなと思います。

信頼できないユーザーにHTMLを入力させるのは、ちょっとでもミスや考慮漏れがあったらそのまま脆弱性に繋がるので、完璧なものを作るのはかなりの工数が必要になります(ずっと昔からいろんなところで脆弱性が発生しているアプローチです)


yambejp様からのコメントも参考にdomを使用して、ホワイトリスト外のものをreplaceしてみるというアプローチを考えました。

例えば以下のようなreplaceChild() を使用したとしても、ノードはツリー構造を持っている(と推察)ため単純に含めたくないnodeNameやtagNameの要素をreplaceすることができません。。
やはり手詰まりです。

DOMDocumentでやるなら、一部を置換するというアプローチでは無く、

  1. 空の出力用DOMツリーを用意する
  2. POSTされたHTMLのDOMツリーを再帰的に走査する
  3. 走査した各要素とその属性についてホワイトリスト(タグごとにバリデーション用の関数なりメソッドなりを用意してチェックさせる)に沿ってチェックを行い、ホワイトリストに合致したものは出力用DOMツリーに追加していく
  4. 合致しないものは無視するなり、htmlspecialchars()をかけた上で適当なタグで囲んでツリーに追加

という感じにすると安全性の担保がしやすいですし、タグ単位で詳細なルールを設置しやすいですね。

投稿2021/04/23 09:30

編集2021/04/23 09:38
tanat

総合スコア18727

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

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

m.ts10806

2021/04/23 10:33

>そもそもHTMLで受け取らずMarkdown,JSON,厳密なXMLやその他パースしやすい独自マークアップで受け取る こちらに大賛成です。 同じ置換なら「消す」じゃなく「システム的に規則化された情報に変換する」ほうが安全です。
pegy

2021/04/23 10:55

ありがとうございます。藁をもすがる思いだったので、コメント頂きたい大変ありがたいです。実際には、quill.jsというリッチテキストエディタライブラリというものを使用しており、 ①JSONで受け取る ②HTML構造で受け取る の2パターンがあります。しかしながら実際にはJSONで受け取ったデータが非常に特異な構造をしていて悩んでおりました。 例えば、以下のように②のHTML構造で受け取ることができるデータがあります。 <p>111</p> <p><br></p> <ol> <li>2<strong>22</strong></li> <li>3<em><u>3</u></em><u>3</u></li> <li><span style="background-color: rgb(255, 255, 0);">444</span></li> </ol> <p><br></p> <blockquote> <span style="background-color: rgb(230, 0, 0);">555</span> </blockquote> これは同様に以下のような①JSONデータでも取得できます。 {"ops":[ {"insert":"111\n\n2"}, {"attributes":{"bold":true},"insert":"22"}, {"attributes":{"list":"ordered"},"insert":"\n"}, {"insert":"3"}, {"attributes":{"underline":true,"italic":true},"insert":"3"}, {"attributes":{"underline":true},"insert":"3"}, {"attributes":{"list":"ordered"},"insert":"\n"}, {"attributes":{"background":"#ffff00"},"insert":"444"}, {"attributes":{"list":"ordered"},"insert":"\n"}, {"insert":"\n"}, {"attributes":{"background":"#e60000"},"insert":"555"}, {"attributes":{"blockquote":true},"insert":"\n"} ]} JSON形式でとても整形が自分には困難で諦めた点は、タグの割当です。atrributeに合わせてタグの割当は難しくないのですが、基本的には<p>タグなので\nが出現するたびに</b>に変換してなど考えるのですが、ol、li、blockquoteといったタグはここからは自分の能力では必要な箇所に開始タグを割当てることができないと断念いたしました。
pegy

2021/04/23 11:05

そこで、②のアプローチに変更いたしました。仰るように独自のマークアップということで対応しようと考えました。 因みにこの場合、私が本題で記載している、"<p" → "*^[p"に置換するといった考え方は、お二方が仰るような 「Markdown,JSON,厳密なXMLやその他パースしやすい独自マークアップ」 とは結果的に相違するのでしょうか?この点を是非ご意見をいただきたいです。 最初のお問い合わせにおいては例えば<p>text</p>のように受け取ったHTMLは^[p">text[/p]_*のような形で独自に整形して受け取ることを前提に記載しておりました。 当該独自のマークアップルールに従わない文字列は全てhtmlspecialcharsで廃城して、最終的に独自マークアップのものをデコードしてHTMLを再構築する意図でした。 ただ、本題にも記載の通り、私には"<p"を置換するような術しかわからず、結果的に残ってしまう ">"がhtmlspecialcharsに当たってしまい、HTMLが再構築できないという問題に打ち当たりました。 属性値等も考慮した場合、例えばこのような形式ですが、<p class="hoge"></p>のような形式でpreg_replaceのようなことができなかったという敬意がございます。 その結果、②を前提とした上で、DOMを操作する方法のアドバイスを頂き現状に行きつきました。 いろいろ、自身でできることや調べれることは尽くしたつもりではあるのですが、この現状を踏まえてとりうるベストな選択肢について、ご助言を賜ることができれば、大変ありがたいです。
tanat

2021/04/23 11:29 編集

> 因みにこの場合、私が本題で記載している、"<p" → "*^[p"に置換するといった考え方は、お二方が仰るような ちょっと意図が読み切れていないのですが、おそらくは違います。 回答での意図は「エンドユーザーにHTML以外のマークアップで入力させる or JavaScriptでHTML以外のマークアップ変換してからPOSTさせる」 です。 例えば、 <p>test</p> あれば {"tag":"p","text":"test","arg":null} みたいな感じのJSONをフォームに入力させるなり、JavaScriptで変換してからサーバ側で受け取るという感じですね。
tanat

2021/04/23 11:28

ただ、 quill.js を使うことが必須なのであれば上記の方法は使えないですね。 quill.jsを使うのであれば、(多分ですが)出力時にquill.jsを使って安全に出力するような方法もある気がします。 無ければ、JSONをサーバサイドで頑張って安全なHTMLに変換するのに工数を掛けるのが現実的だと思います。 もしくは、出力が扱いやすい他のwysiwygエディタを探してみるのも悪くない選択肢だと思います。 あとは、根本的な話として、信頼できないエンドユーザーにHTMLを入力させていいの?という要件の見直しでしょうか。
tanat

2021/04/23 11:43

ざっと調べてみただけなので間違いがあるかもですが、 1. JSONで保存する 2. 出力側の画面でもquill.jsを使う。1で保存したJSONをquill.jsに渡す。 3. 出力側の画面ではhttps://quilljs.com/docs/configuration/ を参照してreadonlyにする 4. 同様にformats https://quilljs.com/docs/formats/ でホワイトリストを設定する? という感じで安全に出力できるんでは?という気がします。 (そのあたりの責任分界点がどこにあるかはドキュメントをしっかり読み込んでみてください)
pegy

2021/04/23 11:45

コメント有難うございます。 はい、ライブラリから見直すかという問題になるので、①か②の何かで考えていました。 少なくとも、要件を変えずに進むのであれば①から頑張ってHTMLを再構築するということですね。。 かなり癖のあるJSONデータで正規表現による処理が避けられない顛末に不安定が残りますが。。 おそらくこのteratailの質問エディタは自前で作っていて、お二人が仰る仕様であると推察しています
tanat

2021/04/23 12:27

> 少なくとも、要件を変えずに進むのであれば①から頑張ってHTMLを再構築するということですね。。 コメントが入れ違いになったものと思いますが、 一つ前のコメントで触れた、表示側もquill.jsで行う方法も試されるかと良いかと思いますよ
pegy

2021/04/23 14:13 編集

コメントありがとうございます。 "出力側の画面でもquill.jsを使う"の意味が全くわからずに、APIを見ていたらsetContns()というまさにその作業をしてくれるAPIを見つけることができました。。自分の経験や能力の浅さで、ライブラリにはそんな機能までは提供してくれないだろうと盲信していたので、本当に助かりました。2週間で辿り着くけたので涙ですが、お恥ずかしい限りです。 彼らが提供するJSONデータからオリジナルのHTMLをAPIで容易に構築できることが理解できたので、まさに責任分界点という観点からも一般論としてお尋ねできればと思います。 ドキュメントは今まさに読み解き始めているので、本当に個別のお話ではなく、一般論としてです。 上記APIでHTMLの再構築が実現できることにしても、このAPIのformatsを利用するホワイトリストを作成するにしてもクライアントサイド(JS)のお話なので、悪意あるユーザーの入力を防ぐことはできないと考えております。特にJSの中身を解析されればいくらでも裏を取られる可能性があると理解しております。 例えば、アンカータグのhref属性に"javascript: alert('danger')"を埋め込んだ場合には以下のようなJSONが生成され、 ({"ops":[{"insert":"111\n"},{"attributes":{"link":"javascript: alert('danger');"},"insert":"222"},{"insert":"\n"}]}); これをsetContns()で吐き出したら、確かに以下のような形で安全に出力されました。 <a href="about:blank" target="_blank">222</a> それでも、一般的にははサーバーサイドにおいても、厳しいチェックをかけるという感覚でクライアントサイドのAPIにはXSSの観点からは全面的に依拠はしないという感覚で正しいでしょうか? 例えば、上の例では、サーバーサイドでできることは、JSONデータのattributesを正規表現などを用いて解析することや、DOMでattributeをそれぞれチェックするなどが考えられると思いますが、このようなケースでのサーバーサイドのチェックという観点を実務的な感覚を最後にご質問させてください。 ※具体的にどのようなチェックをするという事まで聞くのは図々しいと思っているので、当然厳しくチェックするなど、抽象的なご回答でもとてもありがたいです。 ご迷惑をおかけし申し訳ございません。
tanat

2021/04/23 14:56 編集

> それでも、一般的にははサーバーサイドにおいても、厳しいチェックをかけるという感覚でクライアントサイドのAPIにはXSSの観点からは全面的に依拠はしないという感覚で正しいでしょうか? この辺はまあ、要件次第かと思いますよ。 (ちゃんとドキュメントを読んでないので仮定ですが)quill.jsがJSONから安全なHTMLの出力を保証するという仕様であった場合、 クライアントサイドでのチェックと言えど、他人に対して影響を与えることは非常に困難というか基本的には無理です。 quill.jsに脆弱性が発見された場合はその基本から外れる訳ですが、その際にどうするかというと 脆弱性発見時に被害が発生することをどの程度までなら諦められるか という方針を要件(主にサービス継続性とリスクと運用コストに関する要件)を満たす形で策定すし、それに沿った対応をする必要があります。 例えば、「サービス継続性も被害の発生も妥協してはいけない」とした場合はご認識の通りサーバサイドでJSONの値のバリデーションを行い、quill.js自体の脆弱性が発見されても原理的には安全な状態を保つという選択肢になります。(それでも完全に危険性をゼロにするのは難しいですが) 一方で、quill.jsの外でバリデーションを行うということは、quill.jsの仕様が変わったらサービスが動作しなくなるというサービス継続性に関するリスクが発生するので、常にquill.jsの更新の動向に目を光らせて、ベータ版や非安定板でテストをし続ける必要があります。 とすると、将来に渡ってランニングコストが発生する訳で・・・ 個人的には、そこまで諦められないなら最初からquill.jsを使うのを諦めたら?とライブラリ選定の妥当性や、そもそもの仕様(信頼できないエンドユーザーに危険性のある入力をさせる)に疑いを持つと思います。 被害が発生しなければ最悪サービスは止まってもいい と割り切れるなら、サーバサイドで厳しくバリデーションをしておいて、サービスが止まっちゃったら慌てて対応する。という感じで良いでしょう。 という感じで、 無限にお金があるなら厳しくチェックすればいいですが、 普通はそうではないので、リスクとコストと要件のバランスを取って決める。場合によっては仕様から考え直す。 という感じになります。
pegy

2021/04/23 15:52

ご回答をいただきありがとうございます。 大変よくわかりました。確かに仰る通り、quill.jsが未来永劫同じような形式でJSONデータを出力してくれる保証はないなどあまり想定に含めておりませんでした。そうなると、サーバーサイドのバリデーションの有効性も保守しなければならないと考えると、最終的には「リスクとコストと要件のバランスを取って決める。場合によっては仕様から考え直す」というところに帰着することが、とても、よくわかりました。 よく検討して判断したいと思います。 最後になりますが、アドバイスをいただけましたことに心から御礼を申し上げます!
guest

0

ホワイトリストでやるなら、タグを削除するのではなく、
リストに該当しないすべての「<」と「>」を削除すればよいのでは?
どうしてもというならDomdocumentでしょうけど

投稿2021/04/23 04:24

yambejp

総合スコア116734

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

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

pegy

2021/04/23 04:55

コメントありがとうございます。 その手続が具体的にどうしても思いつかないのですが、ヒントをいただけますでしょうか?イメージとしてはホワイトリスト配列に入っている以外の"<" ">"になると思うのですが、そこがpreg_replace等でも思いつきません。 また、その先の話になろうかと思いますが、ホワイトリスト方式で全て例外の"<" ">"にを削除したとしたら、htmlspecialcharsをせずに出力するということになりますよね。(削除後の文字列をhtmlspecialcharsして出力するとホワイトリストタグも全て置換されてしまうため、上記コードのような方法に思い至りました。)盲目的にユーザー入力値をhtmlspecialcharsすべきとは思っていないのですが、落とし穴がないかという点について、少し不安があり、2点目はお尋ねした次第です。 よろしくお願い申し上げます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問