IEが強制終了(落ちる)バグを報告したときの内容を公開します。

以前のIEのバグ報告の結末で言った、
“同じ原因に起因する同じ現象や解説をネット上で発見した(された)とき。”
が該当しましたので、ここにそのときの情報を公開します。
Microsoftからの返信メールについては、以前の記事をご覧ください。

発見先:
http://d.hatena.ne.jp/shogo4405/20060903/1157257268

以下、途中を省略している部分があります。

3/23にMicrosoftに送信したメール


==============
脆弱性に関して
==============
■ 概要
InternetExplorerで(JScriptの)特定のコードが実行されると突然終了する。



★発生方法
JScriptでブラウザ組み込みのネイティブオブジェクトの持つメソッドを
ポインタとしてfor-inでそのメソッドの持つプロパティを列挙しようとすると
InternetExplorerが突然終了する。



突然終了とは、(OS側で用意していると思われる)共通のMSに対するエラー
報告(送信)画面が出る。
環境2の場合のみ、その後ダイアログが出現してから落ちる。



なお、(当然だが)同じプロセスのIEのみが落ちる。



※ただし、スクリプトレットでは発生しない(!!)



★発生タイミング
特定のコード(上記)が実行されたとき。
ファイルの読み込み時(=ページロード時)かイベント発生時かは問わない。



★実証コード
例えば以下のHTMLファイルを開き、EXECをクリックします。
<html>
<head>
<script language="JScript">
function test() {
  for(var x in window.open) ;
}
</script>
</head>
<body>
<div onclick="test();">EXEC</div>
</body>
</html>



★実証コード(スクリプトレット)
以下をアドレスバーに入れてEnterでもIEは落ちない。
javascript:void( (function(){for(var x in window.open);})( ) );



脆弱性について
for-inに出会うとその場でIEが落ちます。
これだけでは重大な脆弱性とは考えづらいですが、
(1)必ず発生します。
(2)ほとんど全てのメソッドで発生します。
(3)以前にwindow()の件があった。
これらの理由により、脆弱性に関わる調査する必要があると思われます。
なお、私はいわゆる"攻撃"の方法に関する知識がないので、
脆弱性にどれだけ関わるかを調査することはでませんでした。
マイクロソフト側で調査していただきたいと思います。



★発生するメソッド(確認済みのみ)
以下のメソッドで発生を確認。
window.blur
.focus
.close
.open
.createPopup
.moveBy
.moveTo
.resizeBy
.resizeTo
.navigate
.print
.scroll
.scrollBy
.scrollTo
alert
confirm
prompt
showModalDialog
showModelessDialog
showHelp
setTimeout
setInterval
clearTimeout
clearInterval
document.getElementById
.getElementsByName
.getElementsByTagName
.attachEvent
.detachEvent
.clear
.close
.open
.write
.writeln
.createElement
.createStyleSheet
.elementFromPoint
.hasFocus
history.back
.forward
.go



★発生しないメソッド
多数の確認はしていないが、ECMAScriptで定義されている
メソッドは発生しないようです。




脆弱性を悪用するシナリオ



以前の*******と同様の攻撃コードは実行されないのか?
※攻撃コード自体に対する知識がないので、私には断言できません。



IEユーザーがページを見るだけでそのユーザーのIEを落とすことができます。
または特定のアクションなどでも落とすことが可能です。




■ 回避策



IEが落ちることに関する回避策は、2度とそのページを見ないようにすること。
または、アクティブスクリプトを実行しないように設定すること。

4/5にMicrosoftに送信したメール


========
追加情報
========
すでに調査済みであるとは思いますが、
その後こちらで把握している現象について
追加報告いたします。



前回のメールでの"環境2"において、
window.openをサンプルに下記の調査(※)
及び結果となりました。
(※IEが落ちるかどうかについての調査です!)



◆window["open"]をfor-inにかける
[ソース]
for(var i in window["open"]) ;
⇒結果:NG



◆window.open以下に何か付ける
[ソース]
window.open.hoge = 3;
for(var i in window.open) ;
⇒結果:OK
(JSのエラーが発生し、IEは落ちない)



◆window.open以下に何か付けて、消す
[ソース]
window.open.hoge = 3;
delete window.open.hoge;
for(var i in window.open) ;
⇒結果:OK
(JSのエラーが発生し、IEは落ちない)



◆window.open以下に存在しない何かを消す
[ソース]
delete window.open.hoge;
for(var i in window.open) ;
⇒結果:OK
(JSのエラーが発生し、IEは落ちない)



◆window.open以下の存在しない何かを参照だけする
[ソース]
var x = window.open.hoge;  // undefined
for(var i in window.open) ;
⇒結果:NG



◆window.open以下の存在しない何かを実行する
[ソース]
try {
  window.open.hoge();
} catch(e) {
  // ignore.
}
for(var i in window.open) ;
⇒結果:NG



◆window.open自体を前もって実行する
[ソース]
window.open();
for(var i in window.open) ;
⇒結果:NG



◆window.openを別の変数に入れ、for-inにかける
var wo = window.open;
for(var i in wo) ;
⇒結果:NG



◆window.openを関数の戻り値として受け、for-inにかける
function wo() {
  return window.open;
}
for(var i in wo()) ;
⇒結果:NG



◆別Windowのページから参照で呼び出す
====本体====
function test() {
  for(var i in window.open) ;
}
====別Win(子)パターン1====
opener.test();
====別Win(子)パターン2====
eval("opener.test();");
====別Win(子)パターン3====
with(opener) {
  test();
}
⇒結果:OK(パターン1,2,3とも)
(JSのエラーが発生し、IEは落ちない)



◆別Windowのページから親のsetTimeoutで呼び出す
====本体====
function test() {
  for(var i in window.open) ;
}
====別Win(子)パターン1====
opener.setTimeout("test();", 3000);
====別Win(子)パターン2====
opener.setTimeout(opener.test, 3000);
⇒結果:NG(パターン1,2とも)

document.writeとストリーム

※1157010937の回答の1/4

document.write(以降、writelnも含む)は、
読み込んでいるファイルのストリームの最後に内容を書き出します。

静的なdocument.writeの場合


<html>
<head>
<script type="text/javascript"><!--



var x = 10;



document.write("x = 20;");



alert(x);



//--></script>
</head>
<body>
</body>
</html>
を読み込むとき、alert(x);の直前にx = 20;を書き出すのではありません

このJavaScriptが実行されるとき、HTMLファイルはどこまで読み込まれているかというと、


<html>
<head>
<script type="text/javascript"><!--



var x = 10;



document.write("x = 20;");



alert(x);



//--></script>
までですので、この最後尾にx = 20;が追加され、

<html>
<head>
<script type="text/javascript"><!--



var x = 10;



document.write("x = 20;");



alert(x);



//--></script>x = 20;
</head>
<body>
</body>
</html>
と認識されます。

このとき、ストリームは開いていて、つまりファイルは読み込み中で、その最後尾に追加するのがポイントです。

動的なdocument.writeの場合

では、document.writeで書き出したソースの中にdocument.writeがあったら、それはどうなるのでしょうか?

まずは、最初に書き出したソースに直接document.writeがあった場合


<html>
<head>
<script type="text/javascript"><!--



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript"><!--\r\n';
  str += 'document.write)((new Date())(.toLocaleString());\r\n';
  str += '/' + '/--></' + 'script>';
  str += 'です。</div>\r\n';
  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<script type="text/javascript"><!--
dispTime();
//--></script>
</body>
</html>
これは意図通りに動作するようです。

次に、最初に書き出したソースで外部JSファイルをロードし、そこでdocument.writeしている場合


【dispTime.js】
document.write)((new Date())(.toLocaleString());

<html>
<head>
<script type="text/javascript"><!--



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript" src="./dispTime.js" charset="Shift_JIS"></' + 'script>';
  str += 'です。</div>\r\n';
  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<script type="text/javascript"><!--
dispTime();
//--></script>
</body>
</html>
意図通りに動作しません。
なぜかというと、日時を書きたいdocument.writeが動く時点では、既にストリームに「ゴミ文字列」がたくさん書き込まれてしまっているので、document.writeしてもその後に書き出すだけになります。
ただし、もともと固定でHTMLファイルに書いてある場合には、ちゃんとJSファイルはscriptタグのところで実行されます。

さらに、document.writeするタイミングを2回に分けても無駄です。


<html>
<head>
<script type="text/javascript"><!--



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript" src="./dispTime.js" charset="Shift_JIS"></' + 'script>';
  str += 'です。</div>\r\n';
  document.write(str);
  str = '';

  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<script type="text/javascript"><!--
dispTime();
//--></script>
</body>
</html>
これはJavaScriptのスレッドが(ページごとに)1本しかないためで、仮に「ゴミ文字列」をdocument.writeする前にdispTime.jsを読み込み完了していても、それらを書いたスクリプトが走っている最中です。
そのスレッドが終わってからdispTime.jsの実行の番となるのです。

※(余談)ということは、個人的には外部ファイルを使わない方があるべき姿ではないような気がします。

ファイル読み込み後のdocument.writeの場合

ファイルのロードが終わってしまうと、ストリームが閉じます。
その後でdocument.writeすると、新たなストリームが開かれます。
ブラウザの動作では、新しい白紙ページに遷移して“読み込み中”となり、そこに書き込まれます。


<html>
<head>
<script type="text/javascript"><!--



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript" src="./dispTime.js" charset="Shift_JIS"></' + 'script>';
  str += 'です。</div>\r\n';
  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<input type="button" onclick="dispTime();" value="document.writeする" />
</body>
</html>

まとめ

つまり、document.writeするときにはdocument.writeする時点でのストリームがどうなっているかを意識しなければなりません。

外部JSファイルの動的ロード

※1157010937の回答の2/4

スクリプト処理の中でロードしたいJSファイルを決定し、スクリプトで外部スクリプトファイルの読み込みを行う方法を説明します。

document.writeで読み込む

1157010937の回答の1/4のサンプルの中で、外部JSファイルdispTime.jsを動的に読み込んでいる通り、可能です。
ただし、ストリームが閉じてしまってからは無理です。

innerHTMLで読み込む

この方法は動作しません。
1157010937の回答の3/4のサンプルの中で、「クリックで質問と同じ状況」のクリックがその例となります。
innerHTMLに含まれる<script>タグは動作しないようです。

document.createElementで読み込む

まず、ダミーの<div>タグをdocument.createElementします。
そのinnerHTMLに<script>タグを含むソースを流し込み、ダミーごと本体にDOMでくっつけます。
→この方法では、InternetExplorerではNGでしたが、FireFoxでは動作しました。
1157010937の回答の4/4のサンプルの中で、「クリックで動的ロード」のクリックがその例となります。

では、IEでも動的ロードを可能にするにはどうするかというと、<script>タグをdocument.createElementします。
作成したscript要素の属性に、type,language,src,charset,deferなどを付けて、そのまま本体にくっつけます。
そうするとファイルの読み込み後でも、IEFireFoxも外部JSファイルがロードされます。

後からロードした外部JSファイルでdocument.writeする(1)

※1157010937の回答の3/4

ファイルの読み込み後にロードした外部JSファイルでdocument.writeするときの問題点を洗います。


【writer.js】
// document.writeする
var str = '<div style="background-color:orange;">外部JSファイルによって作成されたHTML要素です。</div>';
document.write(str);



// HTML側からのチェック用
function writer() {
  alert("./writer.jsが読み込まれています。");
}



// ステータスバーでのチェック用
status += " Load!";


<html>
<head>
<script type="text/javascript"><!--



function viewSource(id) {
  if(document.body) {
    alert("ソースビュー " + id + "\n" + document.body.parentNode.innerHTML);
  } else {
    alert("ソースビュー " + id + "\n" + "<body>がまだ出現していない");
  }
}



viewSource(0);



function test(str) {
  document.writeln(str);
}



function writeScriptTag() {
  test('<script type="text/javascript" src="./writer.js" charset="Shift_JIS"></' + 'script>');
}



function writeAfterLoad() {
  test('あいう');
  // writeScriptTag();
}



function writeInInnerHTML() {
  // ↓擬似受信電文
  var str = '<div>scriptタグの直前<script type="text/javascript" src="./writer.js" charset="Shift_JIS"><' + '/script>scriptタグの直後</div>';
  document.getElementById("target").innerHTML = str;
}



viewSource(1);



test('<title>てすと</title>');



viewSource(2);



//--></script>
</head>
<body>



<div><input type="button" onclick="viewSource();" value="ソースチェック" /></div>



<hr />
※HTMLのタグを直接書いてある場合
<div>外部JSファイルの前</div>
<script type="text/javascript" src="./writer.js" charset="Shift_JIS"></script>
<div>外部JSファイルの後</div>



<hr />
※HTMLのタグをdocument.writeした場合
<script type="text/javascript"><!--



  viewSource(10);
test('<div>外部JSファイルの前</div>');
  viewSource(11);
writeScriptTag();
  viewSource(12);
test('<div>外部JSファイルの後</div>');
  viewSource(13);



//--></script>



<hr />



<div onclick="viewSource(20);writeAfterLoad();viewSource(21);">クリックでdocument.write();</div>



<hr />



<div onclick="viewSource(30);writeInInnerHTML();viewSource(31);">クリックで質問と同じ状況</div>



<div id="target" style="border:solid 1px black"></div>



</body>
</html>

※このサンプルは、1157010937の回答の2/4のおさらいができるようになっているので、その部分の説明は省略します。

まず、HTMLのタグが直接書いてあり、外部JSファイルを読み込む<script>タグも直接書いてある場合は、外部JSファイルのdocument.writeも意図した場所に書かれています。

次に、外部JSファイルを読み込む<script>タグを、HTMLのロード中に他のタグと一緒にdocument.writeした場合、一緒に書いた他のタグの後に外部JSファイルのdocument.writeの内容が書かれています。

また、HTMLのロード後にdocument.writeするとページが変わってしまうのでNGです。

さらに、innerHTMLの中に<script>タグがあっても動作しません。

後からロードした外部JSファイルでdocument.writeする(2)

※1157010937の回答の4/4

今まで挙げた問題点をふまえ、以下の要望を満たす方法を考えます。

  • HTMLファイルのロード後に外部JSファイルを読み込む
  • 読み込む外部JSファイルの<script>タグを含んだHTMLのソースが、おおもとのネタ
  • 外部JSファイルは、読み込まれたときにdocument.writeする
  • 外部JSファイルのdocument.writeは、その<script>タグの位置に出したい

けっこう厳しい要求ですが、以下サンプルです。(前回のwriter.jsも使います)


<html>
<head>
<script type="text/javascript"><!--



// 擬似応答電文
var text = '<div>JSファイルの直前<script type="text/javascript" src="./writer.js" charset="Shift_JIS"></' + 'script>JSファイルの直後</div>';




// 以前の方法
function AjaxWriter_Old(str) {
  var targetObj = document.getElementById("target");
  targetObj.innerHTML = str;
}



// JSファイルをHTMLロード後に読み込む方法(IE△)
function ActiveScriptLoad(str) {
  var targetObj = document.getElementById("target");
  var dummyObj = document.createElement("div");
  dummyObj.innerHTML = str;
  targetObj.appendChild(dummyObj);
}



// 解決方法
function AjaxWriter_New(str) {
  var dummyObj = document.createElement("div");
  dummyObj.innerHTML = str;
  var scripts = [];
  (function(node) {
    var children = node.childNodes;
    for(var i = 0; i < children.length; i++) {
      var child = children[i];
      if(child.nodeName.toLowerCase() == "script") {
        scripts[scripts.length] = {src : child.src, type : child.type, charset : child.charset, language : child.language, defer : child.defer};
      } else {
        arguments.callee(child);
      }
    }
  })(dummyObj);
  var name = "ScriptFilePoint";
  var tags = str.replace(/<script.*?<\/script>/ig, '<span name="' + name + '"></span>');
  var targetObj = document.getElementById("target");
  targetObj.innerHTML = tags;
  var spans;
  if(document.all) {
//  spans = document.getElementsByTagName("span");
//  for(var i = spans.length - 1; i >= 0; i--) {
//    if(spans[i].name != name) {
//      spans = spans.slice(0, i).concat(spans.slice)((i + 1), spans.length))(;
//      }
//    }

    spans = [];
    var temp = document.getElementsByTagName("span");
    for(var i = 0; i < temp.length; i++) {
      if(temp[i].name == name) {
        spans[spans.length] = temp[i];
      }
    }
    delete temp;

  } else {
    spans = document.getElementsByName(name);
  }
  for(var i = 0; i < scripts.length; i++) {
    ScriptLoadWithWrite(scripts[i].type, scripts[i].src, scripts[i].charset, spans[i]);
  }
}



// スクリプトのロード(IE○)
function ScriptLoadWithWrite(type, src, charset, baseNode) {
  // writeメソッドの隠蔽
  var nativeWriter  = document.write;
  var nativeWriteln = document.writeln;
  var stringBuffer = "";
  document.write = function(str) {
    clearTimeout(recover.timer);
    recover.timer = setTimeout(recover, 0);
    if(str) {
      stringBuffer += str;
    }
  };
  document.writeln = function(str) {
    clearTimeout(recover.timer);
    recover.timer = setTimeout(recover, 0);
    if(str) {
      stringBuffer += str;
    }
    stringBuffer += "\r\n";
  };
  // scriptタグの生成
  var node = document.createElement("script");
  node.src = src;
  node.type = type;
  node.charset = charset;
  node.onload = recover;
  var next = baseNode.nextSibling;
  if(next) {
    baseNode.parentNode.insertBefore(node, next);
  } else {
    baseNode.parentNode.appendChild(node);
  }
  // writeメソッドの戻し
  function recover() {
    clearTimeout(recover.timer);
    node.onload = null;
    document.write   = nativeWriter;
    document.writeln = nativeWriteln;
    if(stringBuffer) {
      var obj = document.createElement("span");
      obj.innerHTML = stringBuffer;
      var baseNode = node;
      var next = baseNode.nextSibling;
      if(next) {
        baseNode.parentNode.insertBefore(obj, next);
      } else {
        baseNode.parentNode.appendChild(obj);
      }
    }
  }
  recover.timer = setTimeout(recover, 3000);
}



//--></script>
</head>
<body>



<div onclick="ActiveScriptLoad(text);">クリックで動的ロード</div>



<div onclick="AjaxWriter_New(text);">クリックで解決!</div>



<div onclick="AjaxWriter_Old(text);">クリックで比較(以前のもの)</div>



<div id="target" style="border:solid 1px black;"></div>



</body>
</html>
※「クリックで解決」以外は前回までのおさらいなので、説明省略します。

AjaxWriter_Newメソッド

まず、AjaxWriter_Newメソッドでは、渡されたソースの中の<script>タグを調査します。
ダミーのエレメントを作成し、そのinnerHTMLにソースを流し込み、そこから<script>タグとその情報を抜き出します。

次に、ソースの<script>タグを別のものに置き換えて、目的の場所のinnerHTMLに挿入します。

スクリプトのロードをするためにScriptLoadWithWriteメソッドを呼んで終了です。

ScriptLoadWithWriteメソッド

まず、外部JSファイルをロードする前に、document.writeを隠蔽します。
外部JSファイルがロードされ、そこでdocument.writeされたらその文字列をバッファとして蓄積します。

次に、外部JSファイルをロードします。

最後に、外部JSファイルのロード完了を待って、recoverメソッドで隠蔽したdocument.writeを戻し、バッファの文字列を任意のオブジェクトの直後に書き出します。
recoverメソッドが呼ばれるまでは、本体側でdocument.writeが使えません。

ちなみに、ソースコードの最後のタイマーですが、外部JSファイルのロードに時間がかかったり、応答なしの場合にrecoverメソッドを実行するためのものです。
タイマーを0にすると、外部JSファイルのロードにちょっとでも時間がかかると先にrecoverメソッドが呼ばれ、うまく動きません。
ブラウザでタイムアウトするくらいの長さか、必ず帰ってくるならばその行自体をコメント化します。


個人的な感想…

なんだか、仕様の穴を抜けていくようなコードを書いた気がしてならないです。
途中で、またまたIEを無条件で強制終了させるコードを見つけてしまうし…。
ここまで書いて最後に設計バグを見つけました。
外部JSファイルが複数種類あってそのロード時間がけっこう違うとき、一番速いファイルでrecoverメソッドが走るので、その後のファイルの実行は失敗する気がする。
う゛〜ん、これは許して。

「ファイル名比較集計ツール」という名前で公開します。

複数フォルダのファイル名を正規表現を用いてパターン変換し、パターンごとに集計できるツールです。
ツールに加え、ReadMeとサンプルができたので公開します。
http://www2.u-netsurf.ne.jp/~mug/frame.html?/~mug/application/index.html#Souvenir
http://www2.u-netsurf.ne.jp/~mug/hatena/20060827/FileCompare

でも、あまりおもしろいものでもないですね。

なお、WSHJScriptを利用しています。

フォルダ単位のファイル比較ツール

いつぞやの人力検索で解答したけどオープンされなかったネタ*1を公開しちゃいたいと思う・・・のですが、今日ではなく、明日あさってくらい*2にはと思っています。

ブツの概要は、

  1. N個のフォルダ以下のファイル名について、
  2. 指定されたM個の正規表現により変換した名称で分類し
  3. フォルダ×分類結果のマトリックスで、セルの中身はファイル名

というCSVファイルを生成するものです。

いまは、ほとんど取れない時間の中で、ReadMe.txtを作成しているところです。

*1:サブドメインを変えて見てみれば発見できるはずですね

*2:プロジェクトの業火に焼かれている昨今、やっとまともな休みが取れそうです。