FireFox と window.close() のセキュリティ的な関係について

つい最近、Firefox で windows.close() でウィンドウを閉じることができないんですけど、どうすればいいですか?って聞かれました。そんな馬鹿な?と思ってテストしてみたところ、script で window.open() で開いたページは window.close() で閉じることができるのですが、新規に自分で立ち上げた window は window.close() で閉じることができないようです。



<input type="button" value=" 普通に window.close() で閉じる " onClick="window.close()">

一通りのブラウザで動作検証してみましたが、どうも閉じることができないのは Firefox のみの模様。2005〜2006 年くらいで話題になった問題らしいです。そのときの対処方法としては、こんな風に JavaScript を書いてしのいでいたようです。

window.open('', '_parent', '');
window.close();

とか

window.opener = window;
var win = window.open(location.href, "_self");
win.close();

でも、この技は Firefox2 系までで通用するもので、Firefox3 系ではセキュリティホールって扱いで対処されて閉じなくなってしまいました。Firefox だけ window.close() の処理が違うのもいかがなものか?と思いつつ、ちょっとだけ深追いしました。

- スポンサーリンク -

ちょこちょこっと実験してみたら Firefox2 向けの対策コードでは Firefox3 ではセキュリティパッチが当たっていて動作しません。
「スクリプトはスクリプトによって開かれたウィンドウ以外を閉じることができません。」
なんて警告(Firebug で確認できます)でウィンドウを閉じることはできません。

err001.png

しょうがないので Firefox3.5 のソースコードを眺めてみました。

mozilla/dom/src/base/nsGlobalWindow.cpp 内の nsGlobalWindow::Close() を見てみます。赤文字でコメントを足してみましたが、script で開かれた window で無い場合の処理が最初の方で走ります。

NS_IMETHODIMP
nsGlobalWindow::Close()
{
  FORWARD_TO_OUTER(Close, (), NS_ERROR_NOT_INITIALIZED);

  if (IsFrame() || !mDocShell || IsInModalState()) {
    // window.close() is called on a frame in a frameset, on a window
    // that's already closed, or on a window for which there's
    // currently a modal dialog open. Ignore such calls.

    return NS_OK;
  }

  if (mHavePendingClose) {
    // We're going to be closed anyway; do nothing since we don't want
    // to double-close
    return NS_OK;
  }

  if (mBlockScriptedClosingFlag)
  {
    // A script's popup has been blocked and we don't want
    // the window to be closed directly after this event,
    // so the user can see that there was a blocked popup.
    return NS_OK;
  }

/*==============================================
★scriptで開かれたwindowで無い場合の処理
==============================================*/
  // Don't allow scripts from content to close windows
  // that were not opened by script
  nsresult rv = NS_OK;
  if (!mHadOriginalOpener && !nsContentUtils::IsCallerTrustedForWrite()) {
    PRBool allowClose =
      nsContentUtils::GetBoolPref("dom.allow_scripts_to_close_windows",
                                  PR_TRUE);
    if (!allowClose) {
      // We're blocking the close operation
      // report localized error msg in JS console
      nsContentUtils::ReportToConsole(
          nsContentUtils::eDOM_PROPERTIES,
          "WindowCloseBlockedWarning",
          nsnull, 0, // No params
          nsnull, // No URI.  Not clear which URI we should be using
                  // here anyway
          EmptyString(), 0, 0, // No source, or column/line number
          nsIScriptError::warningFlag,
          "DOM Window");  // Better name for the category?

      return NS_OK;
    }
  }

/*==============================================
★それ以外の処理
上記処理で引っかからないようにするためには、下記の2つのいずれかが必要
1. nsContentUtils::IsCallerTrustedForWrite の設定
2. mHadOriginalOpener の設定
==============================================*/
  // Ask the content viewer whether the toplevel window can close.
  // If the content viewer returns false, it is responsible for calling
  // Close() as soon as it is possible for the window to close.
  // This allows us to not close the window while printing is happening.

  nsCOMPtr<nsIContentViewer> cv;
  mDocShell->GetContentViewer(getter_AddRefs(cv));
  if (!mInClose && !mIsClosed && cv) {
    PRBool canClose;

    rv = cv->PermitUnload(&canClose);
    if (NS_SUCCEEDED(rv) && !canClose)
      return NS_OK;

    rv = cv->RequestWindowClose(&canClose);
    if (NS_SUCCEEDED(rv) && !canClose)
      return NS_OK;
  }

  // Fire a DOM event notifying listeners that this window is about to
  // be closed. The tab UI code may choose to cancel the default
  // action for this event, if so, we won't actually close the window
  // (since the tab UI code will close the tab in stead). Sure, this
  // could be abused by content code, but do we care? I don't think
  // so...

  PRBool wasInClose = mInClose;
  mInClose = PR_TRUE;

  if (!DispatchCustomEvent("DOMWindowClose")) {
    // Someone chose to prevent the default action for this event, if
    // so, let's not close this window after all...

    mInClose = wasInClose;
    return NS_OK;
  }

  // Flag that we were closed.
  mIsClosed = PR_TRUE;

  nsCOMPtr<nsIJSContextStack> stack =
    do_GetService(sJSStackContractID);

  JSContext *cx = nsnull;

  if (stack) {
    stack->Peek(&cx);
  }

  if (cx) {
    nsIScriptContext *currentCX = nsJSUtils::GetDynamicScriptContext(cx);

    if (currentCX && currentCX == mContext) {
      // We ignore the return value here.  If setting the termination function
      // fails, it's better to fail to close the window than it is to crash
      // (which is what would tend to happen if we did this synchronously
      // here).
      rv = currentCX->SetTerminationFunction(CloseWindow,
                                             static_cast<nsIDOMWindow *>
                                                        (this));
      if (NS_SUCCEEDED(rv)) {
        mHavePendingClose = PR_TRUE;
      }
      return NS_OK;
    }
  }

  
  // We may have plugins on the page that have issued this close from their
  // event loop and because we currently destroy the plugin window with
  // frames, we crash. So, if we are called from Javascript, post an event
  // to really close the window.
  rv = NS_ERROR_FAILURE;
  if (!nsContentUtils::IsCallerChrome()) {
    nsCOMPtr<nsIRunnable> ev = new nsCloseEvent(this);
    rv = NS_DispatchToCurrentThread(ev);
  }
  
  if (NS_FAILED(rv)) {
    ReallyCloseWindow();
    rv = NS_OK;
  } else {
    mHavePendingClose = PR_TRUE;
  }
  
  return rv;
}

とりあえず最初の条件の回避方法を考えてみます。

if (!mHadOriginalOpener && !nsContentUtils::IsCallerTrustedForWrite()) {

mHadOriginalOpener はスクリプト経由で window.open() した場合以外ではフラグが立たないようなので、Firefox2 時代のように window.opener の書き換えがうまくいかない模様。次に nsContentUtils::IsCallerTrustedForWrite の方だけど netscape.security.PrivilegeMangerの定義済み権限を変更することで何とかなりそうです。
参考:netscape.security.PrivilegeMangerの定義済み権限 » LandEscape Graphics

UniversalBrowserRead: 任意のサイトやウィンドウから非公開データを読み取る権限
UniversalBrowserWrite: 任意の開いているウィンドウを変更できる権限
UniversalXPConnect: ソフトウェアを実行あるいはインストールする権限
UniversalPreferencesRead: プログラムの設定を読み取る権限
UniversalPreferencesWrite: プログラムの設定を変更する権限
CapabilityPreferencesAccess: セキュリティ設定による制限を回避する権限
UniversalFileRead: ローカルのファイルを読み込んでリモートへ送信する権限

ってわけで、こんなテストコードを書いてみました。






<input type="button" value=" 普通に window.close() で閉じる " onClick="window.close()"><br>
<input type="button" value=" Firefox2 対策で window.close() で閉じる " onClick="windowclose1()"><br>
<input type="button" value=" 警告が出るけど何とかして window.close() で閉じる " onClick="windowclose2()"><br>

<script language="JavaScript">
function windowclose1() {
    if (navigator.appName == "Microsoft Internet Explorer") {
        this.focus();
        self.opener = this;
        self.close();
    } else {
        window.opener = window;
        var win = window.open(location.href, "_self");
        win.close();
    }
}
function windowclose2() {
    if (navigator.appName == "Microsoft Internet Explorer") {
        this.focus();
        self.opener = this;
        self.close();
    } else {
        // if (!mHadOriginalOpener && !nsContentUtils::IsCallerTrustedForWrite()) { の回避方法
        netscape.security.PrivilegeManager.enablePrivilege('UniversalBrowserWrite');
        window.close();

    }
}
</script>

Firefox3 で実行すればわかるのですが、こんな危険な香りが漂うダイアログが表示されるので、誰も許可をクリックするわけがありません。一応許可するをクリックするとちゃんとウィンドウは閉じることができますが、実用的ではありません。
※と思ったらブログの記事にしてサーバ上のコンテンツとして試すと旨く動かないようです。ローカルだと下記の画像のようになるのですが・・・う〜む。。。

warn001.png

ちなみにソースコードをもう少し読み進むと、

nsContentUtils::GetBoolPref("dom.allow_scripts_to_close_windows", PR_TRUE);

がでてきます。こちらは Firefox のコンフィグの値で設定する部分です。デフォルトは false になっているところを true に変更すれば window.close() が無条件に効くようになります。(というか他のブラウザと同じ挙動になります。)

config001.png

まぁでもユーザにこの手法を強要することはできないので、事実上 Firefox3 系で window.open() で開いたウィンドウ以外のウィンドウを閉じる方法が、今のところ思いつきません。

おしまい。

- スポンサーリンク -