WebCore:
authorweinig@apple.com <weinig@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 8 Jan 2008 01:30:27 +0000 (01:30 +0000)
committerweinig@apple.com <weinig@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 8 Jan 2008 01:30:27 +0000 (01:30 +0000)
        Reviewed by Sam Weinig

        Fixes: http://bugs.webkit.org/show_bug.cgi?id=16523
        <rdar://problem/5657447>

        When a frame is created with the URL "about:blank" or "", it should
        inherit its SecurityOrigin from its opener.  However, once it has
        decided on that SecurityOrigin, it should not change its mind.
        Prior to this patch, several events could induce the frame to change
        its SecurityOrigin, permitting an attacker to inject script into an
        arbitrary SecurityOrigin.

        This patch makes several changes:

        1) Documents refuse to change from one SecurityOrigin to another
           unless explicitly instructed to do so.

        2) Navigating to a JavaScript URL that produces a value
           preserves the current SecurityOrigin explicitly instead of
           relying on the URL to preserve the origin (which fails for
           about:blank URLs and SecurityOrigins with document.domain set).

           Ideally, we should not preserve the URL at all.  Instead, the
           frame's URL should be the JavaScript URL, as in Firefox, but this
           would require changes that are too risky for this patch.  I'll
           file this as a separate issue.

        3) Various methods of navigating to JavaScript URLs were not
           properly handling JavaScript that returned a value (and should
           therefore replace the current document).  This patch unifies
           those code paths with the path that works.

           There are still a handful of bugs relating to the handling of
           JavaScript URLs, but I'll file those as separate issues.

        Tests: http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html
               http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html
               http/tests/security/aboutBlank/xss-DENIED-set-opener.html

        * dom/Document.cpp:
        (WebCore::Document::initSecurityOrigin):
        * dom/Document.h:
        (WebCore::Document::setSecurityOrigin):
        * loader/FrameLoader.cpp:
        (WebCore::FrameLoader::changeLocation):
        (WebCore::FrameLoader::urlSelected):
        (WebCore::FrameLoader::requestFrame):
        (WebCore::FrameLoader::submitForm):
        (WebCore::FrameLoader::executeIfJavaScriptURL):
        (WebCore::FrameLoader::begin):
        * loader/FrameLoader.h:
        * platform/SecurityOrigin.cpp:
        (WebCore::SecurityOrigin::setForURL):
        (WebCore::SecurityOrigin::createForFrame):
        * platform/SecurityOrigin.h:

LayoutTests:

        Reviewed by Sam Weinig.

        Fixes: http://bugs.webkit.org/show_bug.cgi?id=16523

        Adds new LayoutTests for scripting from about:blank windows.  These
        windows should inherit its SecurityOrigin from its opener and should
        refuse to change their origins when their opener changes exogenously
        (the navigate-opener tests) or explicitly (the set-opener test).

        * http/tests/security/aboutBlank: Added.
        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write-expected.txt: Added.
        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html: Added.
        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url-expected.txt: Added.
        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html: Added.
        * http/tests/security/aboutBlank/xss-DENIED-set-opener-expected.txt: Added.
        * http/tests/security/aboutBlank/xss-DENIED-set-opener.html: Added.
        * http/tests/security/resources/innocent-victim-with-notify.html: Added.
        * http/tests/security/resources/innocent-victim.html: Added.
        * http/tests/security/resources/libwrapjs.js: Added.
        * http/tests/security/resources/open-window.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@29266 268f45cc-cd09-0410-ab3c-d52691b4dbfc

19 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html [new file with mode: 0644]
LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html [new file with mode: 0644]
LayoutTests/http/tests/security/aboutBlank/xss-DENIED-set-opener-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/aboutBlank/xss-DENIED-set-opener.html [new file with mode: 0644]
LayoutTests/http/tests/security/resources/innocent-victim-with-notify.html [new file with mode: 0644]
LayoutTests/http/tests/security/resources/innocent-victim.html [new file with mode: 0644]
LayoutTests/http/tests/security/resources/libwrapjs.js [new file with mode: 0644]
LayoutTests/http/tests/security/resources/open-window.html [new file with mode: 0644]
LayoutTests/platform/win/Skipped
WebCore/ChangeLog
WebCore/dom/Document.cpp
WebCore/dom/Document.h
WebCore/loader/FrameLoader.cpp
WebCore/loader/FrameLoader.h
WebCore/platform/SecurityOrigin.cpp
WebCore/platform/SecurityOrigin.h

index 62e6272..39b4e8d 100644 (file)
@@ -1,3 +1,26 @@
+2008-01-07  Adam Barth  <hk9565@gmail.com>
+
+        Reviewed by Sam Weinig.
+
+        Fixes: http://bugs.webkit.org/show_bug.cgi?id=16523
+
+        Adds new LayoutTests for scripting from about:blank windows.  These
+        windows should inherit its SecurityOrigin from its opener and should
+        refuse to change their origins when their opener changes exogenously
+        (the navigate-opener tests) or explicitly (the set-opener test).
+
+        * http/tests/security/aboutBlank: Added.
+        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write-expected.txt: Added.
+        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html: Added.
+        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url-expected.txt: Added.
+        * http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html: Added.
+        * http/tests/security/aboutBlank/xss-DENIED-set-opener-expected.txt: Added.
+        * http/tests/security/aboutBlank/xss-DENIED-set-opener.html: Added.
+        * http/tests/security/resources/innocent-victim-with-notify.html: Added.
+        * http/tests/security/resources/innocent-victim.html: Added.
+        * http/tests/security/resources/libwrapjs.js: Added.
+        * http/tests/security/resources/open-window.html: Added.
+
 2008-01-07  Adele Peterson  <adele@apple.com>
 
         Reviewed by Antti, Adam, and Mitz.
diff --git a/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write-expected.txt b/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write-expected.txt
new file mode 100644 (file)
index 0000000..623e786
--- /dev/null
@@ -0,0 +1,17 @@
+CONSOLE MESSAGE: line 1: Unsafe JavaScript attempt to access frame with URL http://localhost:8000/security/resources/innocent-victim-with-notify.html from frame with URL about:blank. Domains, protocols and ports must match.
+
+CONSOLE MESSAGE: line 1: Undefined value
+This page opens a window to "", injects malicious code, and then navigates its opener to the victim. The opened window then tries to scripts its opener after document.writeing a new document.
+Code injected into window:
+<script>document.write('<script>function write(target, message) { target.document.body.innerHTML = message; }setTimeout(function() {write(window.opener, \'FAIL: XSS was allowed.\');}, 100);setTimeout(function() {write(window.opener.top.frames[1], \'SUCCESS: Window remained in original SecurityOrigin.\');}, 200);setTimeout(function() { if (window.layoutTestController) layoutTestController.globalFlag = true; }, 300);<\/script>');</script>
+
+--------
+Frame: '<!--framePath //<!--frame0-->-->'
+--------
+This page doesn't do anything special (except signal that it has finished loading).
+
+--------
+Frame: '<!--framePath //<!--frame1-->-->'
+--------
+SUCCESS: Window remained in original SecurityOrigin.
diff --git a/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html b/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html
new file mode 100644 (file)
index 0000000..1a73354
--- /dev/null
@@ -0,0 +1,105 @@
+<html>
+<head>
+<script src="../resources/libwrapjs.js"></script>
+<script src="../resources/cross-frame-access.js"></script>
+<script>
+    var code;
+    var openedWindow;
+
+    window.onload = function()
+    {
+        if (window.layoutTestController) {
+            layoutTestController.waitUntilDone();
+            layoutTestController.setCanOpenWindows();
+            layoutTestController.dumpAsText();
+            layoutTestController.dumpChildFramesAsText();
+        }
+
+        var message_fail = 'FAIL: XSS was allowed.';
+        var message_success = 'SUCCESS: Window remained in original SecurityOrigin.';
+
+        var write_func = 'function write(target, message) { target.document.body.innerHTML = message; }';
+
+        var try_attack = 'write(window.opener, ' + libwrapjs.in_string(message_fail) + ');';
+        var attack = 'setTimeout(function() {' + try_attack + '}, 100);';
+
+        var try_control = 'write(window.opener.top.frames[1], ' + libwrapjs.in_string(message_success) + ');';
+        var control = 'setTimeout(function() {' + try_control + '}, 200);';
+
+        var sigDone = 'setTimeout(function() { if (window.layoutTestController) layoutTestController.globalFlag = true; }, 300);';
+
+        var payload = 'document.write(' + libwrapjs.in_string(libwrapjs.in_script_tag(write_func + attack + control + sigDone)) + ');';
+        code = libwrapjs.in_script_tag(payload);
+        log("Code injected into window:");
+        log(code);
+
+        if (window.layoutTestController) {
+            setTimeout(pollForTest1, 1);
+        } else {
+            log("To run the test, click the button below when the frames finish loading.");
+            var button = document.createElement("button");
+            button.appendChild(document.createTextNode("Run Test"));
+            button.onclick = runTest;
+            document.body.appendChild(button);
+        }
+    }
+    
+    pollForTest1 = function()
+    {
+        if (!layoutTestController.globalFlag) {
+            setTimeout(pollForTest1, 1);
+            return;
+        }
+        runTest1();
+    }
+
+    runTest1 = function() {
+        frames[0].openWindow();
+        openedWindow = frames[0].openedWindow;
+
+        if (window.layoutTestController)
+            layoutTestController.globalFlag = false;
+
+        frames[0].location = 'http://localhost:8000/security/resources/innocent-victim-with-notify.html';
+
+        setTimeout(pollForTest2, 1);
+    }
+
+    pollForTest2 = function()
+    {
+        if (!layoutTestController.globalFlag) {
+            setTimeout(pollForTest2, 1);
+            return;
+        }
+        runTest2();
+    }
+
+    runTest2 = function()
+    {
+        openedWindow.document.write(code);
+        openedWindow.document.close();
+        if (window.layoutTestController) {
+            layoutTestController.globalFlag = false;
+            setTimeout(pollForDone, 1);
+        }
+    }
+
+    pollForDone = function()
+    {
+        if (!layoutTestController.globalFlag) {
+            setTimeout(pollForDone, 1);
+            return;
+        }
+        closeWindowAndNotifyDone(openedWindow);
+    }
+</script>
+</head>
+<body>
+<div>This page opens a window to &quot;&quot;, injects malicious code, and
+then navigates its opener to the victim.  The opened window then tries to
+scripts its opener after <code>document.write</code>ing a new document.</div>
+<pre id="console"></pre>
+<iframe style="border: solid 3px red;" src="../resources/open-window.html"></iframe>
+<iframe style="border: solid 3px green;" src="../resources/innocent-victim.html"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url-expected.txt b/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url-expected.txt
new file mode 100644 (file)
index 0000000..adff9e1
--- /dev/null
@@ -0,0 +1,17 @@
+CONSOLE MESSAGE: line 1: Unsafe JavaScript attempt to access frame with URL http://localhost:8000/security/resources/innocent-victim-with-notify.html from frame with URL about:blank. Domains, protocols and ports must match.
+
+CONSOLE MESSAGE: line 1: Undefined value
+This page opens a window to "", injects malicious code, and then navigates its opener to the victim. The opened window then tries to scripts its opener after reloading itself as a javascript URL.
+Code injected into window:
+<script>window.location = 'javascript:\'<script>function write(target, message) { target.document.body.innerHTML = message; }setTimeout(function() {write(window.opener, \\\'FAIL: XSS was allowed.\\\');}, 100);setTimeout(function() {write(window.opener.top.frames[1], \\\'SUCCESS: Window remained in original SecurityOrigin.\\\');}, 200);setTimeout(function() { if (window.layoutTestController) layoutTestController.globalFlag = true; }, 300);<\\\/script>\''</script>
+
+--------
+Frame: '<!--framePath //<!--frame0-->-->'
+--------
+This page doesn't do anything special (except signal that it has finished loading).
+
+--------
+Frame: '<!--framePath //<!--frame1-->-->'
+--------
+SUCCESS: Window remained in original SecurityOrigin.
diff --git a/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html b/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html
new file mode 100644 (file)
index 0000000..8fe676b
--- /dev/null
@@ -0,0 +1,106 @@
+<html>
+<head>
+<script src="../resources/libwrapjs.js"></script>
+<script src="../resources/cross-frame-access.js"></script>
+<script>
+    var code;
+    var openedWindow;
+
+    window.onload = function()
+    {
+        if (window.layoutTestController) {
+            layoutTestController.waitUntilDone();
+            layoutTestController.setCanOpenWindows();
+            layoutTestController.dumpAsText();
+            layoutTestController.dumpChildFramesAsText();
+        }
+
+        var message_fail = 'FAIL: XSS was allowed.';
+        var message_success = 'SUCCESS: Window remained in original SecurityOrigin.';
+
+        var write_func = 'function write(target, message) { target.document.body.innerHTML = message; }';
+
+        var try_attack = 'write(window.opener, ' + libwrapjs.in_string(message_fail) + ');';
+        var attack = 'setTimeout(function() {' + try_attack + '}, 100);';
+
+        var try_control = 'write(window.opener.top.frames[1], ' + libwrapjs.in_string(message_success) + ');';
+        var control = 'setTimeout(function() {' + try_control + '}, 200);';
+
+        var sigDone = 'setTimeout(function() { if (window.layoutTestController) layoutTestController.globalFlag = true; }, 300);';
+
+        var payload = 'window.location = ' + libwrapjs.in_javascript_document(write_func + attack + control + sigDone);
+        code = libwrapjs.in_script_tag(payload);
+        log("Code injected into window:");
+        log(code);
+
+        if (window.layoutTestController) {
+            setTimeout(pollForTest1, 1);
+        } else {
+            log("To run the test, click the button below when the frames finish loading.");
+            var button = document.createElement("button");
+            button.appendChild(document.createTextNode("Run Test"));
+            button.onclick = runTest;
+            document.body.appendChild(button);
+        }
+    }
+    
+    pollForTest1 = function()
+    {
+        if (!layoutTestController.globalFlag) {
+            setTimeout(pollForTest1, 1);
+            return;
+        }
+        runTest1();
+    }
+
+    runTest1 = function() {
+        frames[0].openWindow();
+        openedWindow = frames[0].openedWindow;
+
+        if (window.layoutTestController)
+            layoutTestController.globalFlag = false;
+
+        frames[0].location = 'http://localhost:8000/security/resources/innocent-victim-with-notify.html';
+
+        setTimeout(pollForTest2, 1);
+    }
+
+    pollForTest2 = function()
+    {
+        if (!layoutTestController.globalFlag) {
+            setTimeout(pollForTest2, 1);
+            return;
+        }
+        runTest2();
+    }
+
+    runTest2 = function()
+    {
+        openedWindow.document.write(code);
+        openedWindow.document.close();
+        if (window.layoutTestController) {
+            layoutTestController.globalFlag = false;
+            setTimeout(pollForDone, 1);
+        }
+    }
+
+    pollForDone = function()
+    {
+        if (!layoutTestController.globalFlag) {
+            setTimeout(pollForDone, 1);
+            return;
+        }
+        closeWindowAndNotifyDone(openedWindow);
+    }
+</script>
+</head>
+<body>
+<div>This page opens a window to &quot;&quot;, injects malicious code, and
+then navigates its opener to the victim.  The opened window then tries to
+scripts its opener after reloading itself as a <code>javascript</code>
+URL.</div>
+<pre id="console"></pre>
+<iframe style="border: solid 3px red;" src="../resources/open-window.html"></iframe>
+<iframe style="border: solid 3px green;" src="../resources/innocent-victim.html"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-set-opener-expected.txt b/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-set-opener-expected.txt
new file mode 100644 (file)
index 0000000..da867ab
--- /dev/null
@@ -0,0 +1,20 @@
+CONSOLE MESSAGE: line 1: Unsafe JavaScript attempt to access frame with URL http://localhost:8000/security/resources/innocent-victim.html from frame with URL about:blank. Domains, protocols and ports must match.
+
+CONSOLE MESSAGE: line 1: Undefined value
+This page opens a window to "", injects malicious code, and then uses window.open.call to set its opener to the victim. The opened window then tries to scripts its opener.
+Code injected into window:
+<script>function write(target, message) { target.document.body.innerHTML = message; }
+setTimeout(function() {write(window.opener.top.frames[0], 'FAIL: XSS was allowed.');}, 100);
+setTimeout(function() {write(window.opener.top.frames[1], 'SUCCESS: Window remained in original SecurityOrigin.');}, 200);
+setTimeout(function() { if (window.layoutTestController) layoutTestController.globalFlag = true; }, 300);</script>
+
+--------
+Frame: '<!--framePath //<!--frame0-->-->'
+--------
+This page doesn't do anything special.
+
+--------
+Frame: '<!--framePath //<!--frame1-->-->'
+--------
+SUCCESS: Window remained in original SecurityOrigin.
diff --git a/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-set-opener.html b/LayoutTests/http/tests/security/aboutBlank/xss-DENIED-set-opener.html
new file mode 100644 (file)
index 0000000..5ef1405
--- /dev/null
@@ -0,0 +1,76 @@
+<html>
+<head>
+<script src="../resources/libwrapjs.js"></script>
+<script src="../resources/cross-frame-access.js"></script>
+<script>
+    var code;
+    var openedWindow;
+
+    window.onload = function()
+    {
+        if (window.layoutTestController) {
+            layoutTestController.waitUntilDone();
+            layoutTestController.setCanOpenWindows();
+            layoutTestController.dumpAsText();
+            layoutTestController.dumpChildFramesAsText();
+        }
+
+        var message_fail = 'FAIL: XSS was allowed.';
+        var message_success = 'SUCCESS: Window remained in original SecurityOrigin.';
+
+        var write_func = 'function write(target, message) { target.document.body.innerHTML = message; }\n';
+
+        var try_attack = 'write(window.opener.top.frames[0], ' + libwrapjs.in_string(message_fail) + ');';
+        var attack = 'setTimeout(function() {' + try_attack + '}, 100);\n';
+
+        var try_control = 'write(window.opener.top.frames[1], ' + libwrapjs.in_string(message_success) + ');';
+        var control = 'setTimeout(function() {' + try_control + '}, 200);\n';
+
+        var sigDone = 'setTimeout(function() { if (window.layoutTestController) layoutTestController.globalFlag = true; }, 300);';
+
+        var payload = write_func + attack + control + sigDone;
+        code = libwrapjs.in_script_tag(payload);
+        log("Code injected into window:");
+        log(code);
+
+        if (window.layoutTestController) {
+            runTest();
+        } else {
+            log("To run the test, click the button below when the frames finish loading.");
+            var button = document.createElement("button");
+            button.appendChild(document.createTextNode("Run Test"));
+            button.onclick = runTest;
+            document.body.appendChild(button);
+        }
+    }
+
+    runTest = function()
+    {
+        openedWindow = window.open('', 'attacker');
+        openedWindow.document.write(code);
+        openedWindow.document.close();
+
+        window.open.call(frames[0], '', 'attacker');
+
+        setTimeout(pollForDone, 1);
+    }
+
+    pollForDone = function()
+    {
+        if (!layoutTestController.globalFlag) {
+            setTimeout(pollForDone, 1);
+            return;
+        }
+        closeWindowAndNotifyDone(openedWindow);
+    }
+</script>
+</head>
+<body>
+<div>This page opens a window to &quot;&quot;, injects malicious code, and
+then uses <code>window.open.call</code> to set its opener to the victim.
+The opened window then tries to scripts its opener.</div>
+<pre id="console"></pre>
+<iframe style="border: solid 3px red;" src="http://localhost:8000/security/resources/innocent-victim.html"></iframe>
+<iframe style="border: solid 3px green;" src="../resources/innocent-victim.html"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/resources/innocent-victim-with-notify.html b/LayoutTests/http/tests/security/resources/innocent-victim-with-notify.html
new file mode 100644 (file)
index 0000000..a2b750f
--- /dev/null
@@ -0,0 +1,14 @@
+<html>
+<head>
+    <script>
+        onload = function()
+        {
+            if (window.layoutTestController)
+                layoutTestController.globalFlag = true;
+        }
+    </script>
+</head>
+<body>
+This page doesn't do anything special (except signal that it has finished loading).
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/resources/innocent-victim.html b/LayoutTests/http/tests/security/resources/innocent-victim.html
new file mode 100644 (file)
index 0000000..b6bd503
--- /dev/null
@@ -0,0 +1,5 @@
+<html>
+<body>
+This page doesn't do anything special.
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/resources/libwrapjs.js b/LayoutTests/http/tests/security/resources/libwrapjs.js
new file mode 100644 (file)
index 0000000..6d51ca1
--- /dev/null
@@ -0,0 +1,62 @@
+// Library for wraping JavaScript code for different evaluation contexts.
+
+var libwrapjs = {
+  transform_each_character: function(str, transform) {
+    var result = new Array();
+    for (var i=0; i < str.length; ++i)
+      result.push(transform(str.charAt(i)));
+    return result.join('');
+  },
+
+  escape_for_single_quote: function(str) {
+    function transform(ch) {
+      if (ch == "\\")
+        return "\\\\";
+      if (ch == "/")
+        return "\\/";
+      if (ch == "'")
+        return "\\'";
+      return ch;
+    }
+    return this.transform_each_character(str, transform);
+  },
+
+  escape_for_html: function(str) {
+    function transform(ch) {
+      if (ch == "<")
+        return "&lt;";
+      if (ch == ">")
+        return "&gt;";
+      if (ch == "&")
+        return "&amp;";
+      if (ch == "\n")
+        return "<br />"
+      if (ch == "\"")
+        return "&quot;";
+      return ch;
+    }
+    return this.transform_each_character(str, transform);
+  },
+
+  in_string: function(code) {
+    return "'" + this.escape_for_single_quote(code) + "'";
+  },
+
+  in_script_tag: function(code) {
+    return "<script>" + code + "</scr" + "ipt>";
+  },
+
+  in_document_write: function(code) {
+    return "document.write(" + this.in_string(this.in_script_tag(code)) + ")";
+  },
+
+  in_javascript_url: function(code) {
+    return this.in_string("javascript:void(" + code + ")");
+  },
+
+  in_javascript_document: function(code) {
+    return this.in_string("javascript:" +
+               this.in_string(this.in_script_tag(code)));
+  }
+};
+
diff --git a/LayoutTests/http/tests/security/resources/open-window.html b/LayoutTests/http/tests/security/resources/open-window.html
new file mode 100644 (file)
index 0000000..8d3e19d
--- /dev/null
@@ -0,0 +1,22 @@
+<html>
+<head>
+<script>
+    window.onload = function()
+    {
+        if (window.layoutTestController)
+            layoutTestController.globalFlag = true;
+    } 
+
+    var windowURL = '';
+    var openedWindow;
+    function openWindow()
+    {
+        openedWindow = window.open(windowURL);
+    }
+</script>
+</head>
+<body>
+<button onclick="openWindow()">Open Window</button>
+</body>
+</html>
+
index 5ca144c..60c4519 100644 (file)
@@ -196,6 +196,9 @@ http/tests/security/javascriptURL/xss-ALLOWED-from-javascript-url-window-open.ht
 http/tests/security/javascriptURL/xss-ALLOWED-to-javascript-url-window-open.html
 http/tests/security/javascriptURL/xss-DENIED-from-javascript-url-in-foreign-domain-window-open.html
 http/tests/security/javascriptURL/xss-DENIED-to-javascript-url-in-foreign-domain-window-open.html
+http/tests/security/aboutBlank/xss-DENIED-set-opener.html
+http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html
+http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html
 
 # DRT is not fully implemented in boomer <rdar://problem/5128261>
 fast/dom/Window/setting-properties-on-closed-window.html
index 3817928..6d6c744 100644 (file)
@@ -1,3 +1,61 @@
+2008-01-07  Adam Barth  <hk9565@gmail.com>
+
+        Reviewed by Sam Weinig
+
+        Fixes: http://bugs.webkit.org/show_bug.cgi?id=16523
+        <rdar://problem/5657447>
+
+        When a frame is created with the URL "about:blank" or "", it should
+        inherit its SecurityOrigin from its opener.  However, once it has
+        decided on that SecurityOrigin, it should not change its mind.
+        Prior to this patch, several events could induce the frame to change
+        its SecurityOrigin, permitting an attacker to inject script into an
+        arbitrary SecurityOrigin.
+
+        This patch makes several changes:
+
+        1) Documents refuse to change from one SecurityOrigin to another
+           unless explicitly instructed to do so.
+
+        2) Navigating to a JavaScript URL that produces a value
+           preserves the current SecurityOrigin explicitly instead of
+           relying on the URL to preserve the origin (which fails for
+           about:blank URLs and SecurityOrigins with document.domain set).
+
+           Ideally, we should not preserve the URL at all.  Instead, the
+           frame's URL should be the JavaScript URL, as in Firefox, but this
+           would require changes that are too risky for this patch.  I'll
+           file this as a separate issue.
+
+        3) Various methods of navigating to JavaScript URLs were not
+           properly handling JavaScript that returned a value (and should
+           therefore replace the current document).  This patch unifies
+           those code paths with the path that works.
+
+           There are still a handful of bugs relating to the handling of
+           JavaScript URLs, but I'll file those as separate issues.
+
+        Tests: http/tests/security/aboutBlank/xss-DENIED-navigate-opener-document-write.html
+               http/tests/security/aboutBlank/xss-DENIED-navigate-opener-javascript-url.html
+               http/tests/security/aboutBlank/xss-DENIED-set-opener.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::initSecurityOrigin):
+        * dom/Document.h:
+        (WebCore::Document::setSecurityOrigin):
+        * loader/FrameLoader.cpp:
+        (WebCore::FrameLoader::changeLocation):
+        (WebCore::FrameLoader::urlSelected):
+        (WebCore::FrameLoader::requestFrame):
+        (WebCore::FrameLoader::submitForm):
+        (WebCore::FrameLoader::executeIfJavaScriptURL):
+        (WebCore::FrameLoader::begin):
+        * loader/FrameLoader.h:
+        * platform/SecurityOrigin.cpp:
+        (WebCore::SecurityOrigin::setForURL):
+        (WebCore::SecurityOrigin::createForFrame):
+        * platform/SecurityOrigin.h:
+
 2008-01-07  Adele Peterson  <adele@apple.com>
 
         Forgot to check in these changes in my last checkin.
index 178eaa8..2f07533 100644 (file)
@@ -3716,6 +3716,9 @@ bool Document::useSecureKeyboardEntryWhenActive() const
 
 void Document::initSecurityOrigin()
 {
+    if (m_securityOrigin && !m_securityOrigin->isEmpty())
+        return;  // m_securityOrigin has already been initialized.
+
     m_securityOrigin = SecurityOrigin::createForFrame(m_frame);
 }
 
index 1ede43e..78f0c04 100644 (file)
@@ -849,6 +849,11 @@ public:
     void initSecurityOrigin();
     SecurityOrigin* securityOrigin() const { return m_securityOrigin.get(); }
 
+    // Explicitly override the security origin for this document.
+    // Note: It is dangerous to change the security origin of a document
+    //       that already contains content.
+    void setSecurityOrigin(SecurityOrigin* o) { m_securityOrigin = o; }
+
     bool processingLoadEvent() const { return m_processingLoadEvent; }
 
 #if ENABLE(DATABASE)
index c5e5538..0721099 100644 (file)
@@ -374,18 +374,6 @@ void FrameLoader::changeLocation(const String& url, const String& referrer, bool
 
 void FrameLoader::changeLocation(const KURL& url, const String& referrer, bool lockHistory, bool userGesture)
 {
-    if (url.deprecatedString().find("javascript:", 0, false) == 0) {
-        String script = KURL::decode_string(url.deprecatedString().mid(strlen("javascript:")));
-        JSValue* result = executeScript(script, userGesture);
-        String scriptResult;
-        if (getString(result, scriptResult)) {
-            begin(m_URL);
-            write(scriptResult);
-            end();
-        }
-        return;
-    }
-
     ResourceRequestCachePolicy policy = (m_cachePolicy == CachePolicyReload) || (m_cachePolicy == CachePolicyRefresh)
         ? ReloadIgnoringCacheData : UseProtocolCachePolicy;
     ResourceRequest request(url, referrer, policy);
@@ -395,16 +383,13 @@ void FrameLoader::changeLocation(const KURL& url, const String& referrer, bool l
 
 void FrameLoader::urlSelected(const ResourceRequest& request, const String& _target, Event* triggeringEvent, bool lockHistory, bool userGesture)
 {
+    if (executeIfJavaScriptURL(request.url(), userGesture))
+        return;
+
     String target = _target;
     if (target.isEmpty() && m_frame->document())
         target = m_frame->document()->baseTarget();
 
-    const KURL& url = request.url();
-    if (url.deprecatedString().startsWith("javascript:", false)) {
-        executeScript(KURL::decode_string(url.deprecatedString().mid(strlen("javascript:"))), true);
-        return;
-    }
-
     FrameLoadRequest frameRequest(request, target);
 
     if (frameRequest.resourceRequest().httpReferrer().isEmpty())
@@ -442,7 +427,7 @@ bool FrameLoader::requestFrame(HTMLFrameOwnerElement* ownerElement, const String
         return false;
 
     if (!scriptURL.isEmpty())
-        frame->loader()->replaceContentsWithScriptResult(scriptURL);
+        frame->loader()->executeIfJavaScriptURL(scriptURL);
 
     return true;
 }
@@ -518,7 +503,7 @@ void FrameLoader::submitForm(const char* action, const String& url, PassRefPtr<F
     DeprecatedString urlString = u.deprecatedString();
     if (urlString.startsWith("javascript:", false)) {
         m_isExecutingJavaScriptFormAction = true;
-        executeScript(KURL::decode_string(urlString.mid(strlen("javascript:"))));
+        executeIfJavaScriptURL(u);
         m_isExecutingJavaScriptFormAction = false;
         return;
     }
@@ -727,15 +712,27 @@ void FrameLoader::didExplicitOpen()
         m_URL = m_frame->document()->url();
 }
 
-void FrameLoader::replaceContentsWithScriptResult(const KURL& url)
+bool FrameLoader::executeIfJavaScriptURL(const KURL& url, bool userGesture)
 {
-    JSValue* result = executeScript(KURL::decode_string(url.deprecatedString().mid(strlen("javascript:"))));
+    if (!url.deprecatedString().startsWith("javascript:", false))
+        return false;
+
+    String script = KURL::decode_string(url.deprecatedString().mid(strlen("javascript:")));
+    JSValue* result = executeScript(script, userGesture);
+
     String scriptResult;
     if (!getString(result, scriptResult))
-        return;
-    begin();
+        return true;
+
+    SecurityOrigin* currentSecurityOrigin = 0;
+    if (m_frame->document())
+        currentSecurityOrigin = m_frame->document()->securityOrigin();
+
+    begin(m_URL, true, currentSecurityOrigin);
     write(scriptResult);
     end();
+
+    return true;
 }
 
 JSValue* FrameLoader::executeScript(const String& script, bool forceUserGesture)
@@ -874,8 +871,12 @@ void FrameLoader::begin()
     begin(KURL());
 }
 
-void FrameLoader::begin(const KURL& url, bool dispatch)
+void FrameLoader::begin(const KURL& url, bool dispatch, SecurityOrigin* origin)
 {
+    // We need to take a reference to the security origin because |clear|
+    // might destroy the document that owns it.
+    RefPtr<SecurityOrigin> forcedSecurityOrigin = origin;
+
     bool resetScripting = !(m_isDisplayingInitialEmptyDocument && m_frame->document() && m_frame->document()->securityOrigin()->isSecureTransitionTo(url));
     clear(resetScripting, resetScripting);
     if (dispatch)
@@ -907,6 +908,8 @@ void FrameLoader::begin(const KURL& url, bool dispatch)
     document->setBaseURL(baseurl.deprecatedString());
     if (m_decoder)
         document->setDecoder(m_decoder.get());
+    if (forcedSecurityOrigin)
+        document->setSecurityOrigin(forcedSecurityOrigin.get());
 
     updatePolicyBaseURL();
 
index 395e763..7b5f197 100644 (file)
@@ -75,6 +75,7 @@ namespace WebCore {
     class ResourceLoader;
     class ResourceRequest;
     class ResourceResponse;
+    class SecurityOrigin;
     class SharedBuffer;
     class SubstituteData;
     class TextResourceDecoder;
@@ -309,7 +310,7 @@ namespace WebCore {
         KURL historyURL(int distance);
 
         void begin();
-        void begin(const KURL&, bool dispatchWindowObjectAvailable = true);
+        void begin(const KURL&, bool dispatchWindowObjectAvailable = true, SecurityOrigin* forcedSecurityOrigin = 0);
 
         void write(const char* str, int len = -1, bool flush = false);
         void write(const String&);
@@ -319,6 +320,9 @@ namespace WebCore {
         void setEncoding(const String& encoding, bool userChosen);
         String encoding() const;
 
+        // Returns true if url is a JavaScript URL.
+        bool executeIfJavaScriptURL(const KURL& url, bool userGesture = false);
+
         KJS::JSValue* executeScript(const String& url, int baseLine, const String& script);
         KJS::JSValue* executeScript(const String& script, bool forceUserGesture = false);
 
@@ -476,8 +480,6 @@ namespace WebCore {
         void updatePolicyBaseURL();
         void setPolicyBaseURL(const String&);
 
-        void replaceContentsWithScriptResult(const KURL&);
-
         // Also not cool.
         void stopLoadingSubframes();
 
index 8535c16..153792c 100644 (file)
 
 namespace WebCore {
 
-SecurityOrigin::SecurityOrigin()
+SecurityOrigin::SecurityOrigin(const KURL& url)
     : m_port(0)
     , m_portSet(false)
     , m_noAccess(false)
     , m_domainWasSetInDOM(false)
 {
-}
+    if (url.isEmpty())
+      return;
 
-void SecurityOrigin::clear()
-{
-    m_protocol = String();
-    m_host = String();
-    m_port = 0;
-    m_portSet = false;
-    m_noAccess = false;
-    m_domainWasSetInDOM = false;
-}
+    m_protocol = url.protocol().lower();
 
-bool SecurityOrigin::isEmpty() const
-{
-    return m_protocol.isEmpty();
-}
+    // These protocols do not represent principals.
+    if (m_protocol == "about" || m_protocol == "javascript")
+        m_protocol = String();
 
-void SecurityOrigin::setForURL(const KURL& url)
-{
-    clear();
+    if (m_protocol.isEmpty())
+        return;
 
-    if (url.isEmpty())
-      return;
+    // data: URLs are not allowed access to anything other than themselves.
+    if (m_protocol == "data")
+        m_noAccess = true;
 
-    m_protocol = url.protocol().lower();
     m_host = url.host().lower();
     m_port = url.port();
 
     if (m_port)
         m_portSet = true;
+}
 
-    // data: URLs are not allowed access to anything other than themselves.
-    if (m_protocol == "data")
-        m_noAccess = true;
+bool SecurityOrigin::isEmpty() const
+{
+    return m_protocol.isEmpty();
 }
 
 PassRefPtr<SecurityOrigin> SecurityOrigin::createForFrame(Frame* frame)
 {
-    RefPtr<SecurityOrigin> origin = new SecurityOrigin();
-
     if (!frame)
-        return origin;
+        return new SecurityOrigin(KURL());
 
     FrameLoader* loader = frame->loader();
-    const KURL& securityPolicyURL = loader->url();
-
-    origin->setForURL(securityPolicyURL);
 
-    if (!origin->isEmpty() && origin->m_protocol != "about")
+    RefPtr<SecurityOrigin> origin = new SecurityOrigin(loader->url());
+    if (!origin->isEmpty())
         return origin;
 
-    // In the case of about:blank or javascript: URLs (which create 
-    // documents using the "about" protocol) do we want to use the
-    // parent or openers origin.
+    // If we do not obtain a principal from the URL, then we try to find a
+    // principal via the frame hierarchy.
 
     Frame* openerFrame = frame->tree()->parent();
     if (!openerFrame) {
index 1892444..af498ef 100644 (file)
@@ -50,16 +50,13 @@ namespace WebCore {
         bool canAccess(const SecurityOrigin*) const;
         bool isSecureTransitionTo(const KURL&) const;
 
+        bool isEmpty() const;
         String toString() const;
         
         SecurityOriginData securityOriginData() const;
         
     private:
-        SecurityOrigin();
-        bool isEmpty() const;
-
-        void clear();
-        void setForURL(const KURL& url);
+        SecurityOrigin(const KURL& url);
 
         String m_protocol;
         String m_host;