Block cross-site top-frame navigations from untrusted iframes
authorcdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 10 Jan 2020 03:38:43 +0000 (03:38 +0000)
committercdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 10 Jan 2020 03:38:43 +0000 (03:38 +0000)
https://bugs.webkit.org/show_bug.cgi?id=206027
<rdar://problem/58320516>

Reviewed by Geoffrey Garen.

Source/WebCore:

Block cross-site top-frame navigations from untrusted iframes, unless they have a user gesture.
We already consider third-party iframes as untrusted, we now also treat first-party iframes
as untrusted if they are loaded both third-party scripts & iframes.

Test: http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes.html

* dom/Document.cpp:
(WebCore::Document::canNavigate):
(WebCore::Document::willLoadScriptElement):
(WebCore::Document::willLoadFrameElement):
(WebCore::Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking):
* dom/Document.h:
* dom/ScriptElement.cpp:
(WebCore::ScriptElement::requestClassicScript):
* html/HTMLFrameElementBase.cpp:
(WebCore::HTMLFrameElementBase::openURL):

LayoutTests:

Add layout test coverage.

* http/tests/security/block-top-level-navigations-by-third-party-iframes-expected.txt:
* http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes-expected.txt: Added.
* http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes.html: Added.
* http/tests/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html: Added.
* http/tests/security/resources/navigate-top-to-error-page.js: Added.

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

LayoutTests/ChangeLog
LayoutTests/http/tests/security/block-top-level-navigations-by-third-party-iframes-expected.txt
LayoutTests/http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes.html [new file with mode: 0644]
LayoutTests/http/tests/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html [new file with mode: 0644]
LayoutTests/http/tests/security/resources/navigate-top-to-error-page.js [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/Document.cpp
Source/WebCore/dom/Document.h
Source/WebCore/dom/ScriptElement.cpp
Source/WebCore/html/HTMLFrameElementBase.cpp

index e2dd5d7..f14de73 100644 (file)
@@ -1,3 +1,19 @@
+2020-01-09  Chris Dumez  <cdumez@apple.com>
+
+        Block cross-site top-frame navigations from untrusted iframes
+        https://bugs.webkit.org/show_bug.cgi?id=206027
+        <rdar://problem/58320516>
+
+        Reviewed by Geoffrey Garen.
+
+        Add layout test coverage.
+
+        * http/tests/security/block-top-level-navigations-by-third-party-iframes-expected.txt:
+        * http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes-expected.txt: Added.
+        * http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes.html: Added.
+        * http/tests/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html: Added.
+        * http/tests/security/resources/navigate-top-to-error-page.js: Added.
+
 2020-01-09  Diego Pino Garcia  <dpino@igalia.com>
 
         [GTK] Unreviewed test gardening
index 3d08f69..9354bd4 100644 (file)
@@ -1,7 +1,7 @@
-CONSOLE MESSAGE: line 6: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigations-by-third-party-iframes.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page.html'. The frame attempting navigation of the top-level window is cross-origin and the user has never interacted with the frame.
+CONSOLE MESSAGE: line 6: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigations-by-third-party-iframes.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page.html'. The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame.
 
 CONSOLE MESSAGE: line 6: SecurityError: The operation is insecure.
-CONSOLE MESSAGE: line 6: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigations-by-third-party-iframes.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page.html'. The frame attempting navigation of the top-level window is cross-origin and the user has never interacted with the frame.
+CONSOLE MESSAGE: line 6: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigations-by-third-party-iframes.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page.html'. The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame.
 
 CONSOLE MESSAGE: line 6: SecurityError: The operation is insecure.
 Test blocking of suspicious top-level navigations by a third-party iframe
diff --git a/LayoutTests/http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes-expected.txt b/LayoutTests/http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes-expected.txt
new file mode 100644 (file)
index 0000000..1a7865e
--- /dev/null
@@ -0,0 +1,16 @@
+CONSOLE MESSAGE: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigations-by-untrusted-first-party-iframes.html' from frame with URL 'http://127.0.0.1:8000/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html'. The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame.
+
+CONSOLE MESSAGE: SecurityError: The operation is insecure.
+CONSOLE MESSAGE: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigations-by-untrusted-first-party-iframes.html' from frame with URL 'http://127.0.0.1:8000/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html'. The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame.
+
+CONSOLE MESSAGE: SecurityError: The operation is insecure.
+Test blocking of suspicious top-level navigations by a untrusted first-party iframe
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS All navigations by subframes have been blocked
+PASS successfullyParsed is true
+
+TEST COMPLETE
diff --git a/LayoutTests/http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes.html b/LayoutTests/http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes.html
new file mode 100644 (file)
index 0000000..bc5de14
--- /dev/null
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/js-test-resources/js-test.js"></script>
+<script>
+description("Test blocking of suspicious top-level navigations by a untrusted first-party iframe");
+jsTestIsAsync = true;
+onload = () => {
+    setTimeout(() => {
+        document.getElementById('testFrame').src = "/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html";
+        setTimeout(() => {
+            testPassed("All navigations by subframes have been blocked");
+            finishJSTest();
+        }, 1000);
+    }, 10);
+}
+</script>
+<iframe src="/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html"></iframe>
+<iframe id="testFrame"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html b/LayoutTests/http/tests/security/resources/navigate-top-level-frame-to-failure-page-untrusted-iframe.html
new file mode 100644 (file)
index 0000000..57dfa1d
--- /dev/null
@@ -0,0 +1,8 @@
+<html>
+<body>
+Success! The navigation was blocked
+<iframe src="http://localhost:8000/security/resources/blank.html"></iframe>
+<script src="http://localhost:8000/security/resources/navigate-top-to-error-page.js"></script>
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/resources/navigate-top-to-error-page.js b/LayoutTests/http/tests/security/resources/navigate-top-to-error-page.js
new file mode 100644 (file)
index 0000000..e2e2e68
--- /dev/null
@@ -0,0 +1 @@
+top.location = "http://localhost:8000/security/resources/should-not-have-loaded.html";
index c12786b..23bb5a5 100644 (file)
@@ -1,3 +1,28 @@
+2020-01-09  Chris Dumez  <cdumez@apple.com>
+
+        Block cross-site top-frame navigations from untrusted iframes
+        https://bugs.webkit.org/show_bug.cgi?id=206027
+        <rdar://problem/58320516>
+
+        Reviewed by Geoffrey Garen.
+
+        Block cross-site top-frame navigations from untrusted iframes, unless they have a user gesture.
+        We already consider third-party iframes as untrusted, we now also treat first-party iframes
+        as untrusted if they are loaded both third-party scripts & iframes.
+
+        Test: http/tests/security/block-top-level-navigations-by-untrusted-first-party-iframes.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::canNavigate):
+        (WebCore::Document::willLoadScriptElement):
+        (WebCore::Document::willLoadFrameElement):
+        (WebCore::Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking):
+        * dom/Document.h:
+        * dom/ScriptElement.cpp:
+        (WebCore::ScriptElement::requestClassicScript):
+        * html/HTMLFrameElementBase.cpp:
+        (WebCore::HTMLFrameElementBase::openURL):
+
 2020-01-09  Andres Gonzalez  <andresg_22@apple.com>
 
         Disable accessibility isolated tree for LayoutTests.
index ab775b6..fde5ef2 100644 (file)
@@ -3366,7 +3366,7 @@ bool Document::canNavigate(Frame* targetFrame, const URL& destinationURL)
         return false;
 
     if (isNavigationBlockedByThirdPartyIFrameRedirectBlocking(*targetFrame, destinationURL)) {
-        printNavigationErrorMessage(*targetFrame, url(), "The frame attempting navigation of the top-level window is cross-origin and the user has never interacted with the frame."_s);
+        printNavigationErrorMessage(*targetFrame, url(), "The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame."_s);
         return false;
     }
 
@@ -3456,6 +3456,16 @@ bool Document::canNavigateInternal(Frame& targetFrame)
     return false;
 }
 
+void Document::willLoadScriptElement(const URL& scriptURL)
+{
+    m_hasLoadedThirdPartyScript = m_hasLoadedThirdPartyScript || !securityOrigin().isSameOriginAs(SecurityOrigin::create(scriptURL));
+}
+
+void Document::willLoadFrameElement(const URL& frameURL)
+{
+    m_hasLoadedThirdPartyFrame = m_hasLoadedThirdPartyFrame || !securityOrigin().isSameOriginAs(SecurityOrigin::create(frameURL));
+}
+
 // Prevent cross-site top-level redirects from third-party iframes unless the user has ever interacted with the frame.
 bool Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking(Frame& targetFrame, const URL& destinationURL)
 {
@@ -3475,8 +3485,9 @@ bool Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking(Frame& targ
     if (sandboxFlags() != SandboxNone)
         return false;
 
-    // Only prevent navigations by third-party iframes.
-    if (canAccessAncestor(securityOrigin(), &targetFrame))
+    // Only prevent navigations by third-party iframes or untrusted first-party iframes.
+    bool isUntrustedIframe = m_hasLoadedThirdPartyScript && m_hasLoadedThirdPartyFrame;
+    if (canAccessAncestor(securityOrigin(), &targetFrame) && !isUntrustedIframe)
         return false;
 
     // Only prevent cross-site navigations.
index a55a5bd..2f9bbc7 100644 (file)
@@ -1327,6 +1327,9 @@ public:
     SecurityOrigin& securityOrigin() const { return *SecurityContext::securityOrigin(); }
     SecurityOrigin& topOrigin() const final { return topDocument().securityOrigin(); }
 
+    void willLoadScriptElement(const URL&);
+    void willLoadFrameElement(const URL&);
+
     Ref<FontFaceSet> fonts();
 
     void ensurePlugInsInjectedScript(DOMWrapperWorld&);
@@ -2055,6 +2058,8 @@ private:
     bool m_isRunningUserScripts { false };
     bool m_mayBeDetachedFromFrame { true };
     bool m_shouldPreventEnteringBackForwardCacheForTesting { false };
+    bool m_hasLoadedThirdPartyScript { false };
+    bool m_hasLoadedThirdPartyFrame { false };
 #if ENABLE(APPLE_PAY)
     bool m_hasStartedApplePaySession { false };
 #endif
index 1511751..bb8da96 100644 (file)
@@ -290,7 +290,10 @@ bool ScriptElement::requestClassicScript(const String& sourceURL)
             scriptCharset(),
             m_element.localName(),
             m_element.isInUserAgentShadowTree());
-        if (script->load(m_element.document(), m_element.document().completeURL(sourceURL))) {
+
+        auto scriptURL = m_element.document().completeURL(sourceURL);
+        m_element.document().willLoadScriptElement(scriptURL);
+        if (script->load(m_element.document(), scriptURL)) {
             m_loadableScript = WTFMove(script);
             m_isExternalScript = true;
         }
index 500f715..0988d9a 100644 (file)
@@ -93,6 +93,8 @@ void HTMLFrameElementBase::openURL(LockHistory lockHistory, LockBackForwardList
     if (!parentFrame)
         return;
 
+    document().willLoadFrameElement(parentFrame->document()->completeURL(m_URL));
+
     String frameName = getNameAttribute();
     if (frameName.isNull() && UNLIKELY(document().settings().needsFrameNameFallbackToIdQuirk()))
         frameName = getIdAttribute();