クライアント側でHTMLをサニタイズ/書き換える


82

クロスドメインリクエストを介して読み込まれた外部リソースを表示し、「安全な」コンテンツのみを表示するようにする必要があります。

PrototypeのString#stripScriptsを使用して、スクリプトブロックを削除できます。しかし、onclickまたはなどのハンドラーonerrorはまだ存在しています。

少なくともできるライブラリはありますか

  • スクリプトブロックを削除し、
  • DOMハンドラーを強制終了します。
  • ブラックリストにあるタグ(例:embedまたはobject)を削除します。

では、JavaScript関連のリンクや例はありますか?


12
正規表現でこれを行う可能性があります答え信用しないでくださいstackoverflow.com/questions/1732348/...
ミッコOhtamaa


これはどのように安全ですか?ユーザーはページのJavaScriptを編集できませんか?
ダニエルはモニカを復活させると言います2015

ええ、信頼できるユーザーによる間違いを単に防ごうとしているのでない限り、それは「安全」ではありません。
スコット

回答:


112

2016年の更新:Cajaサニタイザーに基づくGoogleClosureパッケージがあります。

よりクリーンなAPIを備え、最新のブラウザーで使用可能なAPIを考慮して書き直され、ClosureCompilerとの相互作用が向上しています。


恥知らずなプラグイン:徹底的にレビューれたクライアント側のhtmlサニタイザーについては、caja / plugin /html-sanitizer.jsを参照してください。

ホワイトリストに登録されており、ブラックリストに登録されていませんが、ホワイトリストはCajaWhitelistsに従って構成できます。


すべてのタグを削除する場合は、次の手順を実行します。

var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';

var tagOrComment = new RegExp(
    '<(?:'
    // Comment body.
    + '!--(?:(?:-*[^->])*--+|-?)'
    // Special "raw text" elements whose content should be elided.
    + '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
    + '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
    // Regular name
    + '|/?[a-z]'
    + tagBody
    + ')>',
    'gi');
function removeTags(html) {
  var oldHtml;
  do {
    oldHtml = html;
    html = html.replace(tagOrComment, '');
  } while (html !== oldHtml);
  return html.replace(/</g, '&lt;');
}

要素を作成しinnerHTMLinnerTextorを割り当てて取得し、textContentその中のエンティティをエスケープできると人々は言うでしょう。そんなことしたらダメ。ノードがDOMに接続されていない場合でもハンドラー<img src=bogus onerror=alert(1337)>を実行するため、XSSインジェクションに対して脆弱ですonerror


5
すばらしい、ここに小さなドキュメントがあるようです:code.google.com/p/google-caja/wiki/JsHtmlSanitizer
tmcw

3
Caja HTMLサニタイザーコードは見栄えがしますが、いくつかのグルーコードが必要です(隣接cssparser.jsしていますが、さらに重要なのはhtml4オブジェクトです)。さらに、それはグローバルwindowプロパティを汚染します。このコードのWeb用バージョンはありますか?そうでない場合は、別のプロジェクトを作成するよりも、1つを作成して維持するためのより良い方法がありますか?
phihag 2012

1
@ phihag、google-caja-discussで質問すると、パッケージ化されたものを指す場合があります。ウィンドウオブジェクトの汚染は下位互換性のためであると私は信じているので、新しいパッケージバージョンではそれを必要としないかもしれません。
マイクサミュエル

1
すでにウェブブラウザ用のパッケージがあります
phihag 2012

2
@phihagそのパッケージはnodejs用であり、ブラウザー用ではありません。
ジェフリーから

40

Google Caja HTMLサニタイザーは、Webワーカーに埋め込むことで「Web対応」にすることができます。サニタイザーによって導入されたグローバル変数はすべてワーカー内に含まれ、さらに処理は独自のスレッドで行われます。

Web Workersをサポートしていないブラウザーの場合、サニタイザーが機能するための別個の環境としてiframeを使用できます。TimothyChienには、iframeを使用してWeb Workersをシミュレートする、まさにこれを行うポリフィルがあります。

Cajaプロジェクトには、Cajaをスタンドアロンのクライアント側サニタイザーとして使用する方法に関するwikiページがあります。

  • ソースをチェックアウトし、実行してビルドします ant
  • ページにhtml-sanitizer-minified.jsまたはhtml-css-sanitizer-minified.jsを含める
  • コール html_sanitize(...)

ワーカースクリプトは、次の指示に従うだけで済みます。

importScripts('html-css-sanitizer-minified.js'); // or 'html-sanitizer-minified.js'

var urlTransformer, nameIdClassTransformer;

// customize if you need to filter URLs and/or ids/names/classes
urlTransformer = nameIdClassTransformer = function(s) { return s; };

// when we receive some HTML
self.onmessage = function(event) {
    // sanitize, then send the result back
    postMessage(html_sanitize(event.data, urlTransformer, nameIdClassTransformer));
};

(simworkerライブラリを機能させるには、もう少しコードが必要ですが、この説明では重要ではありません。)

デモ:https//dl.dropbox.com/u/291406/html-sanitize/demo.html


素晴らしい答え。ジェフリー、なぜサニタイズをWebワーカーが行う必要があるのか​​説明できますか?
オースティンワン

@AustinWang Webワーカーは厳密には必要ありませんが、サニタイズは計算コストが高くなる可能性があり、ユーザーの操作を必要としないため、このタスクに適しています。(メインの回答にグローバル変数を含めることにも言及しました。)
Jeffery To

このライブラリの適切なドキュメントが見つかりません。要素と属性のホワイトリストをどこで/どのように指定しますか?
AsGoodAsItGets 2016年

で説明したように@AsGoodAsItGets現在のバージョンでのコメントはnameIdClassTransformerすべてのHTML名、要素のIDとクラスのリストのために呼び出されます。戻るnullと属性が削除されます。src / com / google / caja / lang / htmlのJSONファイルを編集することで、ホワイトリストに登録する要素と属性をカスタマイズすることもできます。
ジェフリーから

@JefferyTo申し訳ありませんが、多分私はあまりにも愚かですが、私はそれを取得しません。参照するJSONファイルは、上記の例とデモでは使用されていません。ライブラリをブラウザで使いたいので、デモを見ました。あなたは、修正することができnameIdClassTranformer、すべて拒否するように、たとえば上記の関数を<script>タグにし、受け入れる<b><i>タグ?
AsGoodAsItGets 2016年

20

クライアントを絶対に信用しないでください。サーバーアプリケーションを作成している場合は、クライアントが常に不衛生で悪意のあるデータを送信すると想定します。これは、問題を回避するための経験則です。可能であれば、サーバーコードですべての検証とサニテーションを行うことをお勧めします。これは、(妥当な程度まで)いじられないことがわかっています。おそらく、サーバーサイドWebアプリケーションをクライアントサイドコードのプロキシとして使用できます。これは、サードパーティからフェッチし、クライアント自体に送信する前にサニタイズを行いますか?

[編集]すみません、質問を誤解しました。しかし、私は私のアドバイスを支持します。サーバーに送信する前にサーバーをサニタイズすると、ユーザーはおそらくより安全になります。


19
実際、node.jsの人気が高まるにつれ、javascriptソリューションもサーバーサイドソリューションになる可能性があります。それが私が少なくともここにたどり着いた方法です。それでも、これは生きるための優れたアドバイスです。
ニコラスフリント2011

15

すべての主要なブラウザがサンドボックス化されたiframeをサポートするようになったので、安全であると私が考えるはるかに簡単な方法があります。この種のセキュリティ問題に精通している人々がこの回答をレビューできれば、私はそれが大好きです。

注:この方法は、IE9以前では確実に機能しません。サンドボックスをサポートするブラウザのバージョンについては、この表を参照してください。(注:表には、Opera Miniでは機能しないと書かれているようですが、試してみたところ、機能しました。)

アイデアは、JavaScriptを無効にして非表示のiframeを作成し、信頼できないHTMLを貼り付けて、解析させることです。次に、DOMツリーをたどって、安全と見なされるタグと属性をコピーします。

ここに示されているホワイトリストは単なる例です。ホワイトリストに登録するのに最適なものは、アプリケーションによって異なります。タグと属性のホワイトリストだけでなく、より高度なポリシーが必要な場合は、このサンプルコードではなく、このメソッドで対応できます。

var tagWhitelist_ = {
  'A': true,
  'B': true,
  'BODY': true,
  'BR': true,
  'DIV': true,
  'EM': true,
  'HR': true,
  'I': true,
  'IMG': true,
  'P': true,
  'SPAN': true,
  'STRONG': true
};

var attributeWhitelist_ = {
  'href': true,
  'src': true
};

function sanitizeHtml(input) {
  var iframe = document.createElement('iframe');
  if (iframe['sandbox'] === undefined) {
    alert('Your browser does not support sandboxed iframes. Please upgrade to a modern browser.');
    return '';
  }
  iframe['sandbox'] = 'allow-same-origin';
  iframe.style.display = 'none';
  document.body.appendChild(iframe); // necessary so the iframe contains a document
  iframe.contentDocument.body.innerHTML = input;

  function makeSanitizedCopy(node) {
    if (node.nodeType == Node.TEXT_NODE) {
      var newNode = node.cloneNode(true);
    } else if (node.nodeType == Node.ELEMENT_NODE && tagWhitelist_[node.tagName]) {
      newNode = iframe.contentDocument.createElement(node.tagName);
      for (var i = 0; i < node.attributes.length; i++) {
        var attr = node.attributes[i];
        if (attributeWhitelist_[attr.name]) {
          newNode.setAttribute(attr.name, attr.value);
        }
      }
      for (i = 0; i < node.childNodes.length; i++) {
        var subCopy = makeSanitizedCopy(node.childNodes[i]);
        newNode.appendChild(subCopy, false);
      }
    } else {
      newNode = document.createDocumentFragment();
    }
    return newNode;
  };

  var resultElement = makeSanitizedCopy(iframe.contentDocument.body);
  document.body.removeChild(iframe);
  return resultElement.innerHTML;
};

ここで試してみることができます

この例では、スタイル属性とタグを許可していないことに注意してください。それらを許可した場合は、CSSを解析して、目的に対して安全であることを確認することをお勧めします。

私はこれをいくつかの最新のブラウザー(Chrome 40、Firefox 36 Beta、IE 11、Chrome for Android)でテストし、1つの古いブラウザー(IE 8)でテストして、スクリプトを実行する前に正常に動作することを確認しました。問題のあるブラウザや、見落としているエッジケースがあるかどうかを知りたいと思います。


10
この投稿は、明白で最も単純な解決策のように思われるため、専門家の注目に値します。本当に安全ですか?
pwray 2015年

「JavaScriptを無効にして」非表示のiframeをプログラムで作成するにはどうすればよいですか?私の知る限り、これは不可能です。あなたが分iframe.contentDocument.body.innerHTML = inputがある中、どのようなスクリプトタグが実行されます。
AsGoodAsItGets 2016年

@ AsGoodAsItGets-iframeのサンドボックス属性を検索します。
aldel 2016年

1
@aldel確かに、私はそれについて知りませんでした。IE9がサポートされていないため、私たちにとってはまだ問題があります。あなたの解決策はうまくいくと思いますが、あなたはあなたがsandbox属性に依存していることをあなたの応答で明確にすべきだと思います。
AsGoodAsItGets 2016年

申し訳ありませんが、「すべての主要なブラウザがサンドボックス化されたiframeをサポートするようになりました」という冒頭から明らかだと思いました。さりげないメモを追加します。
aldel 2016年

12

ブラックリストから逃れるためにどこかのブラウザがつまずく可能性のある奇妙なタイプの不正なマークアップをすべて予測することはできないので、ブラックリストに載せないでください。スクリプト/埋め込み/オブジェクトとハンドラーだけでなく、削除する必要のある構造がたくさんあります。

代わりに、HTMLを階層内の要素と属性に解析してから、すべての要素と属性の名前を可能な限り最小限のホワイトリストに対して実行してください。また、通過したURL属性をホワイトリストと照合します(javascriptだけではなく危険なプロトコルがあることに注意してください)。

入力が整形式のXHTMLである場合、上記の最初の部分ははるかに簡単です。

HTMLサニタイズの場合と同様に、それを回避する他の方法を見つけることができる場合は、代わりにそれを行ってください。多くの潜在的な穴があります。主要なウェブメールサービスがこの数年経ってもまだエクスプロイトを見つけているとしたら、何があなたをより良くできると思いますか?


11

つまり、2016年であり、私たちの多くはnpm現在、コードでモジュールを使用していると思います。sanitize-htmlにもありますが、npmの主要なオプションのようです。

この質問に対する他の回答は、自分でロールする方法についての優れた情報を提供しますが、これは十分にトリッキーな問題であるため、十分にテストされたコミュニティソリューションがおそらく最良の回答です。

これをコマンドラインで実行してインストールします。 npm install --save sanitize-html

ES5: var sanitizeHtml = require('sanitize-html'); // ... var sanitized = sanitizeHtml(htmlInput);

ES6: import sanitizeHtml from 'sanitize-html'; // ... let sanitized = sanitizeHtml(htmlInput);


10
2018年ここでは、これは重すぎます(0.5メガバイトの依存関係)
user1464581 2018年

ここでの2020年、sanitize-htmlはNode用であり、私が知る限り、ブラウザーに適したオプションはまだありません
Mick

3

[免責事項:私は著者の一人です]

このために「Webのみ」(つまり「ブラウザが必要」)のオープンソースライブラリhttps://github.com/jitbit/HtmlSanitizertags/attributes/stylesを作成し、「ホワイトリストに登録された」ものを除くすべてを削除しました。

使用法:

var input = HtmlSanitizer.SanitizeHtml("<script> Alert('xss!'); </scr"+"ipt>");

PSは、ブラウザを使用してDOMを解析および操作するため、「純粋なJavaScript」ソリューションよりもはるかに高速に動作します。「純粋なJS」ソリューションに興味がある場合は、https://github.com/punkave/sanitize-html(提携していない)を試してください。


2

上で提案したGoogleCajaライブラリは複雑すぎて、Webアプリケーション用に構成してプロジェクトに含めることができませんでした(したがって、ブラウザーで実行します)。代わりに私が頼ったのは、すでにCKEditorコンポーネントを使用しているため、組み込みのHTMLサニタイズおよびホワイトリスト機能を使用することです。これは構成がはるかに簡単です。したがって、非表示のiframeにCKEditorインスタンスをロードして、次のようなことを行うことができます。

CKEDITOR.instances['myCKEInstance'].dataProcessor.toHtml(myHTMLstring)

確かに、プロジェクトでCKEditorを使用していない場合、コンポーネント自体は約0.5メガバイト(最小化)であるため、これは少しやり過ぎかもしれませんが、ソースがある場合は、コードを分離して実行できます。ホワイトリスト(CKEDITOR.htmlParser?)を作成し、はるかに短くします。

http://docs.ckeditor.com/#!/api

http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor


0

私はあなたの人生からフレームワークを取り除くことをお勧めします、それはあなたにとって長期的に物事を過度に簡単にするでしょう。

cloneNode:ノードのクローンを作成すると、そのすべての属性とその値がコピーされますが、イベントリスナーはコピーされませ

https://developer.mozilla.org/en/DOM/Node.cloneNode

以下は、しばらくの間ツリーウォーカーを使用していて、JavaScriptの最も過小評価されている部分の1つですが、テストされていません。クロールできるノードタイプのリストは次のとおりです。通常、SHOW_ELEMENTまたはSHOW_TEXTを使用します。

http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#Traversal-NodeFilter

function xhtml_cleaner(id)
{
 var e = document.getElementById(id);
 var f = document.createDocumentFragment();
 f.appendChild(e.cloneNode(true));

 var walker = document.createTreeWalker(f,NodeFilter.SHOW_ELEMENT,null,false);

 while (walker.nextNode())
 {
  var c = walker.currentNode;
  if (c.hasAttribute('contentEditable')) {c.removeAttribute('contentEditable');}
  if (c.hasAttribute('style')) {c.removeAttribute('style');}

  if (c.nodeName.toLowerCase()=='script') {element_del(c);}
 }

 alert(new XMLSerializer().serializeToString(f));
 return f;
}


function element_del(element_id)
{
 if (document.getElementById(element_id))
 {
  document.getElementById(element_id).parentNode.removeChild(document.getElementById(element_id));
 }
 else if (element_id)
 {
  element_id.parentNode.removeChild(element_id);
 }
 else
 {
  alert('Error: the object or element \'' + element_id + '\' was not found and therefore could not be deleted.');
 }
}

5
このコードは、cleanする入力がすでに解析されており、ドキュメントツリーに挿入されていることを前提としています。その場合、悪意のあるスクリプトはすでに実行されています。入力は文字列である必要があります。
phihag 2012

次に、DOMフラグメントを送信します。これは、特定の形状または形式でDOM内にあるからといって、実際に実行されたことを意味するわけではありません。彼がAJAXを介してロードしていると仮定すると、これをimportNodeと組み合わせて使用​​できます。
ジョン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.