Hatena::Groupsubtech

ういはるかぜの化学

Saturday, May 19, 2012

[][]HTML+JavaScriptでのMetro style appにおける制限 16:18 HTML+JavaScriptでのMetro style appにおける制限 - ういはるかぜの化学 を含むブックマーク はてなブックマーク - HTML+JavaScriptでのMetro style appにおける制限 - ういはるかぜの化学

去年の9月ごろに書いていたのだけど、ほかに書くことを書いて順を追って公開しようと思っていたら寝かせすぎた…。msWWAがMSAppにかわってたりしたのでその辺を修正して公開。

Metro style appの実装形態としてHTML+JavaScript+CSSを選択することができ、Internet Explorer 10が持つ機能ローカルアプリケーションを実装できます

ローカルアプリケーションとして動くということで通常のブラウザ機能だけでなくネイティブ機能を利用できるようになっていますしかしながらそれと同時に仕様セキュリティ上の理由で一部の機能に制限がもうけられています

ウィンドウ操作

Metro style appでは新規ウィンドウ作成ウィンドウ位置変更など各種ウィンドウ操作は行えません。ウィンドウ操作にはダイアログも含まれます。具体的には以下のメソッドが影響を受けます

  • alert
  • prompt
  • open
  • moveby
  • moveto
  • resizeby
  • resizeto

だだし例外的にアプリケーションからはwindow.closeは実行できます。window.closeはアプリケーションの終了と同じですが基本的には使うべきではないとされています。利用すべき場面は復帰できないエラーが発生した場合に強制終了するといった使い方です。

javascriptスキーム

a要素のjavascriptスキームは動作しないようになっています。これはあまり困りませんね。

解決策

DOMclickイベントや直接書きたい場合にはonclick属性を利用するように書き換えます

innerHTML/outerHTML/insertAdjacentHTML/document.write に渡すことできるHTMLの制限

通常innerHTMLやdocument.write にはHTMLを渡して出力したり要素を生成したりできます

ところがMetro style appの場合にはinnerHTMLなどに渡すことのできるHTMLに制限がかかります。たとえば以下のようなコードを実行しようとします。

<script>
    window.addEventListener('DOMContentLoaded', function () {
        var divE = document.createElement('div');
        divE.innerHTML = "<a onclick='console.log(1)' href='#'>Link</a>";
        document.body.appendChild(divE);
    }, false);
</script>

すると実行時に以下の例外エラーが発生します。

0x800c001c - JavaScript runtime error: Unable to add dynamic content. A script attempted to inject dynamic content, or elements previously modified dynamically, that might be unsafe. For example, using the innerHTML property or the document.write method to add a script element will generate this exception. If the content is safe and from a trusted source, use a method to explicitly manipulate elements and attributes, such as createElement, or use

これはinnerHTMLなどのプロパティメソッドに渡すことのできるHTMLセキュリティ上静的なもの(安全もの)となっていることが求められているためです。静的でないHTMLというのはscript要素やonなんとか属性、form要素などのスクリプト等の動的な要素をふくむもののことです。

許可される要素や属性IE8以降で実装されている window.toStaticHTML というscript要素をはじめとして動的・未知の要素・属性サニタイズをするメソッドを通るものです。Making HTML safer: details for toStaticHTML (HTML) - Windows app developmentにそのリストがありますsvg要素なども通らないので注意が必要です。

この制限は外部からデータを読み込んでそのままアプリケーションに流し込んだ際、悪意あるスクリプトを実行してしまうのを防ぐためにあるものと思われます

Metro style appはローカルアプリケーションなのでコンピュータへのアクセスが行えるようになっていて、たとえばファイル操作するようなスクリプトを含んだHTMLをinnerHTMLで差し込んでしまうとそのローカルアプリケーションの一部として実行されて困ったことが起こる、といった感じですね。

innerHTML以外にも以下のメソッドプロパティにこの制限のかかっています

  • innerHTML
  • outerHTML
  • insertAdjacentHTML
  • pasteHTML
  • document.write / document.writeln
  • DOMParser.pasteFromString
解決策: document.createElementを利用する

innerHTMLなどを使わずcreateElementで要素を作ってごく普通に内容をセットしていく方法です。この場合自前でHTMLパースしない限りは悪意あるスクリプトを取り込んでしまうことはなくなります

逆に自前でHTMLパースしてしまうと潜在的に危険ものも組み立ててしまう可能性があるので注意が必要です。

var divE = document.createElement('div');
var aE = document.createElement('a');
aE.href = "#";
aE.textContent = "Link";
divE.appendChild(aE);
document.body.appendChild(divE);
解決策: window.toStaticHTMLを利用してサニタイズする

innerHTMLなどが受け取って安全とされるものはwindow.toStaticHTMLを通ることを許可されている要素や属性、ということなのでtoStaticHTMLメソッドを利用してサニタイズしてしまます

var divE = document.createElement('div');
// <a onclick="console.log(1)" href="#">Link</a> -> <a href="#">Link</a>
divE.innerHTML = window.toStaticHTML('<a onclick="console.log(1)" href="#">Link</a>');
document.body.appendChild(divE);

toStaticHTMLで静的となったHTMLであればinnerHTMLにセットしてもエラーとならなくなります

解決策: 安全ではない操作として実行する

安全ではない操作を実行する」方法も用意されています。これはinnerHTMLなどに安全ではないHTMLをセットしても動く、普通IEと同様の挙動を実現する方法です。まあ、同様の挙動といっても「安全ではない操作単位での実行となるのでコードを若干修正する必要があります

安全ではない操作の実行にはwindow.MSAppに存在するexecUnsafeLocalFunctionメソッドを利用します。

execUnsafeLocalFunctionメソッド引数関数をとり、その関数を実行している間は各種のチェックが無効となります。たとえば以下のようなコードで動くようになります

<script>
    window.addEventListener('DOMContentLoaded', function () {
        var divE = document.createElement('div');
        MSApp.execUnsafeLocalFunction(function () {
            divE.innerHTML = "<a onclick='console.log(1)' href='#'>Link</a>";
        });
        document.body.appendChild(divE);
    }, false);
</script>

この方法は信頼されている、たとえばアプリケーションローカルリソースを読み込んでのようなデータ差し込む場合にのみ利用すべきです。

またこのinnerHTMLをセットする操作比較的行われる操作のため、Visual Studioなどテンプレートからアプリケーションを作った場合についてくるWinJSライブラリに以下のヘルパーメソッドとして用意されています

<script>
    window.addEventListener('DOMContentLoaded', function () {
        var divE = document.createElement('div');
        WinJS.Utilities.setInnerHTMLUnsafe(divE, "<a onclick='console.log(1)' href='#'>Link</a>");
        document.body.appendChild(divE);
    }, false);
</script>
注意

追記: 以下の点はRelease Previewで変更されて即座にエラーとなるようになりました。

この制限のわかりにくいポイントの一つとして、要素がドキュメントに追加された際にエラーが発生するというところがあります

以下のコードで実際エラーが発生するのはdocument.body.appendChildのタイミングとなります

<script>
    window.addEventListener('DOMContentLoaded', function () {
        var divE = document.createElement('div');
        divE.innerHTML = "<a onclick='console.log(1)' href='#'>Link</a>";
        document.body.appendChild(divE);
    }, false);
</script>

innerHTMLをセットした瞬間ではないのでjQueryを使っている場合などjQueryの内部で突然エラーが発生して悩むことになることがあります

サンプルコード

<!DOCTYPE html>
<meta charset="utf-8">
<title>Unsafe operations</title>
<link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">
<script src="//Microsoft.WinJS.0.6/js/base.js"></script>
<script src="//Microsoft.WinJS.0.6/js/ui.js"></script>
<script>
    window.addEventListener('DOMContentLoaded', function () {
        var divE = document.createElement('div');

        // エラー
        //divE.innerHTML = "<a onclick='console.log(1)' href='#'>Link</a>";
        //document.body.appendChild(divE);

        // エラー
        //divE.outerHTML = "<a onclick='console.log(1)' href='#'>Link</a>";
        //document.body.appendChild(divE);

        // エラー
        //document.write("<script>alert(1);<"+ "/script>");

        // OK
        //divE.innerHTML = window.toStaticHTML("<a onclick='console.log(1)' href='#'>Link</a>");
        //document.body.appendChild(divE);

        // OK
        //WinJS.Utilities.setInnerHTMLUnsafe(divE, "<a onclick='console.log(1)' href='#'>Link</a>");
        //document.body.appendChild(divE);

        // OK
        window.MSApp.execUnsafeLocalFunction(function () { divE.innerHTML = "<a onclick='console.log(1)' href='#'>Link</a>"; });
        document.body.appendChild(divE);
    }, false);
</script>
<body>
</body>