問題の本質を説明するために、HTMLの一般的な配置方法を伝える必要があります。 おそらく一般的な用語で想像したと思いますが、理解に必要な主な点については簡単に説明します。 誰かが待てない場合は、ポイントに直行してください 。
HTMLはハイパーテキストマークアップ言語です。 この言語を話すには、その形式に従う必要があります。そうしないと、書かれたものを読む人はあなたを理解できません。 たとえば、HTMLでは、タグには次の属性があります。
<p name="value">
ここで、 [name]
は属性の名前、 [value]
はその値です。 この記事では、コードの前後に角かっこを使用して、コードの開始位置と終了位置を明確にします。 名前の後に等号があり、その後に引用符で囲まれた値があります。 属性値は、最初の引用文字の直後から始まり、どこにいても次の引用文字の直前で終わります。 これは、 [value]
代わりに[value]
[OOO " ".]
[value]
を書く場合、 name
属性の値は[OOO ]
になり、要素には名前を持つ他の3つの属性[]
、 []
および["."]
、しかし意味なし。
<p name="OOO " "."></p>
これが予期したものでない場合は、属性の値を何らかの方法で変更して、引用符が表示されないようにする必要があります。 あなたが考えることができる最も簡単なことは、単に引用符を切り取ることです。
<p name="OOO ."></p>
HTMLパーサーは値を正しく読み取りますが、問題は値が異なることです。 あなたは[OOO " "]
望んでいましたが、あなたは[OOO .]
を手に入れました。 場合によっては、この違いが重要になることがあります。
値として任意の文字列を指定できるように、HTML言語形式は属性値をエスケープする機能を提供します。 値文字列の引用符の代わりに、一連の文字["]
を記述でき["]
パーサーは、属性値として使用する元の文字列のこの場所に引用符があったことを理解します。 このようなシーケンスはHTMLエンティティと呼ばれます。
<p name="OOO " "."></p>
同時に、実際にソース文字列に文字シーケンス["]
があった場合、パーサーが引用符に変換しないように記述する機会があります。これを行うには、 [&]
記号を文字シーケンス[&]
に置き換え[&]
、つまり、 ["]
代わりに、生テキストに[&quot;]
を記述する必要があります。
元の文字列から2つの引用符の間に記述した文字列への変換は、 明確で可逆的であることがわかります。 これらの変換のおかげで、HTMLタグの属性として、その内容の本質に触れることなく、 任意の行を読み書きできます。 形式に従うだけで、すべてが機能します。
実際、これは私たちが遭遇するほとんどのフォーマットがこのように機能する方法です:構文があり、この構文からコンテンツをエスケープする方法があり、そのようなシーケンスが元の行にある場合にエスケープ文字をエスケープする方法があります。 ほとんどですが、そうではありません...
<script>タグ
<script>タグは、他の言語で記述されたHTMLフラグメントを埋め込むために使用されます。 今日では99%のケースでJavascriptです。 スクリプトは、開始<script>タグの直後から開始し、終了</ script>タグの直前で終了します。 HTMLパーサーはタグの内部を調べません。これは、Javascriptパーサーに渡すテキストの一部にすぎないためです。
Javascriptは、独自の構文を持つ独立した言語であり、一般的に、HTMLに埋め込まれるもののための特別な方法で設計されていません。 その中には、他の言語と同様に、何でもできる文字列リテラルがあります。 また、既に推測しているように、終了</ script>タグを意味する一連の文字が存在する場合があります。
<script> var s = "surprise!</script><script>alert('whoops!')</script>"; </script>
起こるべきこと: s
変数には無害な文字列を割り当てる必要があります。
ここで実際に起こること:変数s
宣言されているスクリプトは、実際には次のように終了します: [var s = "surprise!]
、構文エラーになります。その後のすべてのテキストは純粋なHTMLとして解釈され、テキストを埋め込むことができますマークアップ:この場合、新しい<script>タグが開かれ、悪意のあるコードが実行されます。
属性値に引用符がある場合と同じ効果が得られました。 ただし、属性値とは異なり、<script>タグで元のコンテンツをエスケープする方法はありません。 <script>タグ内のHTMLエンティティは機能せず、変更せずにJavascriptパーサーに渡されます。つまり、エラーが発生したり、意味が変わったりします。 HTML標準では、<script>タグのコンテンツには、どのような形式でも文字シーケンス</ script>を含めることはできません。 また、Javascript標準では、このようなシーケンスが文字列リテラルのどこかにあることを禁止していません。
逆説的な状況が判明しました。 有効なJavascriptを完全に有効な手段で有効なHTMLドキュメントに埋め込んだ後、無効な結果を得ることができます 。
私の意見では、これは実際のアプリケーションの脆弱性につながるHTMLマークアップの脆弱性です。
脆弱性の悪用方法
もちろん、コードを記述するだけでは、</ script>行に何を書くか想像するのは難しく、問題に気付かないでしょう。 少なくとも、構文の強調表示により、タグが事前に閉じられていることがわかります。最大で、作成したコードは開始されず、何が起こったのかを長時間探します。 しかし、これはこの脆弱性の主な問題ではありません。 HTMLを生成するときにJavascriptにコンテンツを埋め込むと問題が発生します。 次に、reactをレンダリングしたサーバー上のアプリケーションコードの頻繁な部分を示します。
<script> window.__INITIAL_STATE__ = <%- JSON.stringify(initialState) %>; </script>
initialState
</ script>は、ユーザーまたは他のシステムからデータが送信される任意の場所に表示できます。 JSON.stringify
は、JSONおよびJavascript形式と完全に一貫しているため、シリアライズ中にそのような文字列を変更しません。したがって、ページにアクセスし、攻撃者がユーザーのブラウザで任意のJavascriptを実行できるようにします。
別の例:
<script> analytics.identify( '<%- user.id.replace(/(\'|\\)/g, "\\$1") %>', '<%- request.HTTP_REFERER.replace(/(\'|\\)/g, "\\$1") %>', ... ); </script>
ここで、サーバーに来たユーザーid
とreferer
は、適切なエスケープで行に書き込まれます。 そして、 user.id
に数字以外のほとんどない場合、 referer
攻撃者は何でもuser.id
ことができます。
しかし、終了タグ</ script>ジョークはそこで終わりません。 開始<script>タグは、その前に文字[<!--]
がある場合も危険です[<!--]
これは、プレーンHTMLでは複数行コメントの開始を示します。 この場合、ほとんどのエディターの構文強調表示は役に立ちません。
<script> var a = 'Consider this string: <!--'; var b = '<script>'; </script> <p>Any text</p> <script> var s = 'another script'; </script>
健康な人とほとんどの構文ハイライトはこのコードで何を見るのでしょうか? 段落の間にある2つの<script>タグ。
病気のHTML5パーサーは何を見ますか? 彼は、2行目から最後の行までのすべてのテキストを含む1つの(!)閉じられていない(!)<script>タグを見ています。
私はこれがなぜこのように機能するのか完全には理解していません[<!--]
文字[<!--]
どこかでHTMLパーサーが開始および終了<script>タグのカウントを開始し、すべてが終了するまでスクリプトの完了を考慮しないことを理解するだけ[<!--]
<script>タグを開きます。 つまり、ほとんどの場合、このスクリプトはページの最後に移動します(誰かが下に別の追加の終了</ script>タグを挿入できない限り、hehe)。 あなたがこれに遭遇したことがないなら、あなたは私が今冗談を言っていると思うかもしれません。 残念ながら、ありません。 上記のサンプルDOMツリーのスクリーンショットは次のとおりです。
最も不愉快なのは、JavaScriptで文字列リテラル内でのみ発生する終了</ script>タグとは異なり、文字シーケンス<!--
および<script
がコード自体で発生する可能性があることです! そして、それらはまったく同じ効果があります。
<script> if (x<!--y) { ... } if ( player<script ) { ... } </script>
あなたはまさに仕様ですか?
HTML仕様は、<script>タグ内で有効な文字シーケンスの使用を禁止することに加えて、それらをHTML内でエスケープする方法を提供していません。また、次のことも推奨します。
このセクションで説明する奇妙な制限を回避する最も簡単で安全な方法は、常に「<!-」を「<\!-」、「<script」を「<\ script」、「</ script」としてエスケープすることです。 "as" <\ / script "これらのシーケンスがスクリプト内のリテラル(たとえば、文字列、正規表現、またはコメント)に現れるとき、および式でそのような構造を使用するコードの記述を避けるため。
「常にエスケープシーケンス」として翻訳できるもの<!--
「as」 <\!--
「、」 <script
"as" <\script
"および" </script
"as" <\/script
" スクリプト内の文字列リテラルに含まれており、コード自体ではこれらの式を避けます。 この勧告は私を感動させます。 ここでは、いくつかの素朴な仮定が一度に行われます。
- 埋め込みスクリプト(これは必ずしもJavaScriptではありません)では、上記の文字シーケンスは文字列リテラル内にあるか、言語構文で簡単に回避できます。
- 文字列リテラルの埋め込みスクリプトでは、非特殊文字をエスケープできますが、これによりリテラルの値は変更されません。
- スクリプトを埋め込む人は誰でもスクリプトの種類を知っており、その構文を深く理解し、その構造を変更することができます。
そして、最初の2つのポイントが少なくともJavascriptで実行される場合、最後のポイントはそれでも実行されません。 HTMLにスクリプトを挿入するのは常に資格のある人ではなく、何らかのHTMLジェネレーターである可能性があります。 ブラウザ自体がこれを処理できない方法の例を次に示します。
var script = document.createElement('script') script.innerText = 'var s = "</script><script>alert(\'whoops!\')</script>"'; console.log(script.outerHTML); >>> <script>var s = "</script><script>alert('whoops!')</script>"</script>
ご覧のとおり、シリアル化された要素を持つ行は、元の要素に似た要素に解析されません。 一般的な場合の変換DOMツリー→HTMLテキストは、一意ではなく、元に戻せません。 一部のDOMツリーは、単にHTMLソーステキストとして表すことができません。
問題を回避する方法は?
既に理解しているように、JavascriptをHTMLに安全に挿入する方法はありません。 ただし、JavascriptをHTMLに安全に埋め込む方法はあります(違いを感じてください)。 確かに、このため、特にテンプレートエンジンを使用してデータを挿入する場合は、<script>タグ内に何かを記述している間は常に非常に注意する必要があります。
まず、文字列リテラルではなく、ソーステキスト内の文字[<!-- <script>]
極端に小さい可能性(縮小後でも)。 あなた自身がこのようなことを書くことはまずありません。攻撃者が<script>タグに何かを直接書くことができる場合、これらの文字の導入は最後にあなたを悩ますでしょう。
文字列に文字を埋め込む問題が残っています。 この場合、仕様に書かれているように、必要なことは、すべての「 <!--
」を「 <\!--
」、「 <script
」を「 <\script
」、および「 </script
」を「 <\/script
"。 しかし、問題は、 JSON.stringify()
を使用して何らかの構造を出力する場合、すべての文字列リテラルを見つけてその中の何かをスクリーニングするために、後で再び解析したくないということです。 また、状況が異なるため、この問題がすでに考慮されているシリアル化のために他のパッケージを使用することはお勧めしませんが、常に自分自身を守りたいし、解決策は普遍的でなければなりません。 したがって、文字/および!をエスケープすることをお勧めします。 シリアル化後にバックスラッシュを使用します。 これらの文字は、行内を除いてJSONで見つけることができないため、単純な置換は絶対に安全です。 これにより、文字シーケンス「 <script
」は変更されませんが、単独で発生しても危険ではありません。
<script> window.__INITIAL_STATE__ = <%- JSON.stringify(initialState).replace(/(\/|\!)/g, "\\$1") %>; </script>
同様に、個々の行をエスケープできます。
もう1つのヒントは、<script>タグには何も埋め込まないことです。 データを挿入する変換が明確で可逆的な場所にデータを保存します。 たとえば、他の要素の属性。 確かに、かなり汚く見え、文字列でのみ動作します。JSONは個別に解析する必要があります。
<var id="s" data="surprise!</script><script>alert("whoops!")</script>"></var> <script> var s = document.getElementById('s').getAttribute('data'); console.log(s); </script>
しかし、良い方法で、もちろん、アプリケーションを通常どおりに開発し、地雷原を慎重に歩かない場合は、HTMLにスクリプトを埋め込む信頼できる方法が必要です。 したがって、安全ではないため、<script>タグを完全に拒否する正しい判断を検討します。
<safescript>タグ
埋め込みスクリプトを使用しない場合、何をしますか? もちろん、すべてのスクリプトを外部から接続することはオプションではありません。HTMLドキュメント内にデータを持つ何らかのJavascriptがあると便利な場合があります。余分なHTTPリクエストはなく、サーバー側で追加のルートを作成する必要はありません。
したがって、新しいタグを導入することを提案します-<safescript>、そのコンテンツは通常のHTMLルールに完全に従う-HTMLエンティティはコンテンツをエスケープするために動作します-したがって、スクリプトを埋め込むことは絶対に安全です。
<safescript> var s = "surprise!</script><script>alert('whoops!')</script>"; </safescript> <safescript> var a = 'Consider this string: <!--'; var b = '<script>'; </safescript>
ブラウザでこのタグが実装されるのを待つ必要はありません。 すぐに使用できる非常にシンプルなセーフスクリプトの親友を作成しました。 これに必要なものは次のとおりです。
<script type="text/javascript" src="/static/safescript.js"></script> <style type="text/css">safescript {display: none !important}</style>
<safescript>内のコードは、ひどくて異常に見えます。 しかし、これはHTML自体に入るコードです。 使用するテンプレートエンジンで、タグを挿入し、そのすべてのコンテンツをエスケープする単純なフィルターを作成できます。 Djangoテンプレートエンジンのコードは次のようになります。
{% safescript %} var s = "surprise!</script><script>alert('whoops!')</script>"; {% endsafescript %} {% safescript %} var a = 'Consider this string: <!--'; var b = '<script>'; {% endsafescript %}
このアプローチにより、Javascriptエスケープを忘れ、多くの脆弱性を回避できます。 そして、HTML仕様を開発している人たちがそのようなスクリプトをベースセットに追加したり、HTMLでのスクリプトの安全でない埋め込みの問題を解決する他の方法を思いついたら素晴らしいでしょう。