Saturday, May 19, 2012
■ [Metro][WinJS]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スキームは動作しないようになっています。これはあまり困りませんね。
解決策
DOMのclickイベントや直接書きたい場合には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 (Windows Store apps using JavaScript and HTML) (Windows)にそのリストがあります。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ライブラリに以下のヘルパーメソッドとして用意されています。
- WinJS.Utilities.setInnerHTML function (Windows)
- WinJS.Utilities.setInnerHTMLUnsafe function (Windows)
- WinJS.Utilities.insertAdjacentHTML function (Windows)
- WinJS.Utilities.insertAdjacentHTMLUnsafe function (Windows)
- WinJS.Utilities.setOuterHTML function (Windows)
- WinJS.Utilities.setOuterHTMLUnsafe function (Windows)
<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で変更されて即座にエラーとなるようになりました。
<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>
サンプルコード
<!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>