Title: [270373] trunk
Revision
270373
Author
[email protected]
Date
2020-12-02 15:01:29 -0800 (Wed, 02 Dec 2020)

Log Message

iframe with `sandbox=allow-top-navigation-by-user-activation` can navigate top frame when the user interacts with an iframe from another origin
https://bugs.webkit.org/show_bug.cgi?id=219413
<rdar://problem/64887657>

Reviewed by Geoffrey Garen.

Source/WebCore:

An iframe with `sandbox=allow-top-navigation-by-user-activation` can navigate the top frame when the user
interacts with an frame from another origin. This is not strict enough and does not match the behavior of
Chrome.

In Chrome, the user activation is only valid for the purpose of navigation if the user interacted with either:
- The iframe triggering the navigation
- A descendant iframe of the iframe triggering the navigation
- A frame from the same origin as the iframe triggering the navigation

This patch aligns our behavior with Chrome's.

Test: http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html

* dom/Document.cpp:
(WebCore::Document::canNavigateInternal):
* dom/UserGestureIndicator.cpp:
(WebCore::UserGestureToken::UserGestureToken):
(WebCore::UserGestureToken::isValidForDocument const):
(WebCore::UserGestureIndicator::processingUserGesture):
* dom/UserGestureIndicator.h:
(WebCore::UserGestureToken::create):

LayoutTests:

Add layout test coverage.

* http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture-expected.txt: Added.
* http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html: Added.
* http/tests/security/resources/navigate-top-level-frame-to-failure-page-via-message-handler.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (270372 => 270373)


--- trunk/LayoutTests/ChangeLog	2020-12-02 23:00:55 UTC (rev 270372)
+++ trunk/LayoutTests/ChangeLog	2020-12-02 23:01:29 UTC (rev 270373)
@@ -1,3 +1,17 @@
+2020-12-02  Chris Dumez  <[email protected]>
+
+        iframe with `sandbox=allow-top-navigation-by-user-activation` can navigate top frame when the user interacts with an iframe from another origin
+        https://bugs.webkit.org/show_bug.cgi?id=219413
+        <rdar://problem/64887657>
+
+        Reviewed by Geoffrey Garen.
+
+        Add layout test coverage.
+
+        * http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture-expected.txt: Added.
+        * http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html: Added.
+        * http/tests/security/resources/navigate-top-level-frame-to-failure-page-via-message-handler.html: Added.
+
 2020-12-02  Ryan Haddad  <[email protected]>
 
         [macOS] imported/w3c/web-platform-tests/webaudio/the-audio-api/the-audiobuffer-interface/ctor-audiobuffer.html is a flaky failure

Added: trunk/LayoutTests/http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture-expected.txt (0 => 270373)


--- trunk/LayoutTests/http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture-expected.txt	2020-12-02 23:01:29 UTC (rev 270373)
@@ -0,0 +1,13 @@
+CONSOLE MESSAGE: Unsafe _javascript_ attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page-via-message-handler.html'. The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set.
+
+CONSOLE MESSAGE: SecurityError: The operation is insecure.
+Test blocking of top-level navigations by an iframe with `sandbox=allow-top-navigation-by-user-activation` when the user gesture is propagated from another context.
+
+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
+

Added: trunk/LayoutTests/http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html (0 => 270373)


--- trunk/LayoutTests/http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html	                        (rev 0)
+++ trunk/LayoutTests/http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html	2020-12-02 23:01:29 UTC (rev 270373)
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src=""
+<script>
+description("Test blocking of top-level navigations by an iframe with `sandbox=allow-top-navigation-by-user-activation` when the user gesture is propagated from another context.");
+jsTestIsAsync = true;
+_onload_ = () => {
+    setTimeout(() => {
+        internals.withUserGesture(() => {
+            document.getElementsByTagName('iframe')[0].contentWindow.postMessage("foo", "*");
+        });
+        setTimeout(() => {
+            testPassed("All navigations by subframes have been blocked");
+            finishJSTest();
+        }, 100);
+    }, 10);
+}
+</script>
+<iframe id="testFrame" src="" sandbox="allow-forms allow-pointer-lock allow-popups-to-escape-sandbox allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation"></iframe>
+</body>
+</html>

Added: trunk/LayoutTests/http/tests/security/resources/navigate-top-level-frame-to-failure-page-via-message-handler.html (0 => 270373)


--- trunk/LayoutTests/http/tests/security/resources/navigate-top-level-frame-to-failure-page-via-message-handler.html	                        (rev 0)
+++ trunk/LayoutTests/http/tests/security/resources/navigate-top-level-frame-to-failure-page-via-message-handler.html	2020-12-02 23:01:29 UTC (rev 270373)
@@ -0,0 +1,9 @@
+<html>
+<body>
+<script>
+_onmessage_ = (e) => {
+    top.location = "http://127.0.0.1:8000/security/resources/should-not-have-loaded.html";
+}
+</script>
+</body>
+</html>

Modified: trunk/Source/WebCore/ChangeLog (270372 => 270373)


--- trunk/Source/WebCore/ChangeLog	2020-12-02 23:00:55 UTC (rev 270372)
+++ trunk/Source/WebCore/ChangeLog	2020-12-02 23:01:29 UTC (rev 270373)
@@ -1,5 +1,35 @@
 2020-12-02  Chris Dumez  <[email protected]>
 
+        iframe with `sandbox=allow-top-navigation-by-user-activation` can navigate top frame when the user interacts with an iframe from another origin
+        https://bugs.webkit.org/show_bug.cgi?id=219413
+        <rdar://problem/64887657>
+
+        Reviewed by Geoffrey Garen.
+
+        An iframe with `sandbox=allow-top-navigation-by-user-activation` can navigate the top frame when the user
+        interacts with an frame from another origin. This is not strict enough and does not match the behavior of
+        Chrome.
+
+        In Chrome, the user activation is only valid for the purpose of navigation if the user interacted with either:
+        - The iframe triggering the navigation
+        - A descendant iframe of the iframe triggering the navigation
+        - A frame from the same origin as the iframe triggering the navigation
+
+        This patch aligns our behavior with Chrome's.
+
+        Test: http/tests/security/block-top-level-navigations-by-sandboxed-iframe-with-propagated-user-gesture.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::canNavigateInternal):
+        * dom/UserGestureIndicator.cpp:
+        (WebCore::UserGestureToken::UserGestureToken):
+        (WebCore::UserGestureToken::isValidForDocument const):
+        (WebCore::UserGestureIndicator::processingUserGesture):
+        * dom/UserGestureIndicator.h:
+        (WebCore::UserGestureToken::create):
+
+2020-12-02  Chris Dumez  <[email protected]>
+
         Block suspicious top level navigations by iframes even if sandbox=allow-top-navigation is specified
         https://bugs.webkit.org/show_bug.cgi?id=219408
         <rdar://problem/71049726>

Modified: trunk/Source/WebCore/dom/Document.cpp (270372 => 270373)


--- trunk/Source/WebCore/dom/Document.cpp	2020-12-02 23:00:55 UTC (rev 270372)
+++ trunk/Source/WebCore/dom/Document.cpp	2020-12-02 23:01:29 UTC (rev 270373)
@@ -3477,8 +3477,11 @@
     if (!isSandboxed(SandboxTopNavigation) && &targetFrame == &m_frame->tree().top())
         return true;
 
+    // The user gesture only relaxes permissions for the purpose of navigating if its impacts the current document.
+    bool isProcessingUserGestureForDocument = UserGestureIndicator::processingUserGesture(m_frame->document());
+
     // ii. A frame can navigate its top ancestor when its 'allow-top-navigation-by-user-activation' flag is set and navigation is triggered by user activation.
-    if (!isSandboxed(SandboxTopNavigationByUserActivation) && UserGestureIndicator::processingUserGesture() && &targetFrame == &m_frame->tree().top())
+    if (!isSandboxed(SandboxTopNavigationByUserActivation) && isProcessingUserGestureForDocument && &targetFrame == &m_frame->tree().top())
         return true;
 
     // iii. A sandboxed frame can always navigate its descendants.
@@ -3495,14 +3498,13 @@
 
     // 2. Otherwise, if B is a top-level browsing context, and is one of the ancestor browsing contexts of A, then:
     if (m_frame != &targetFrame && &targetFrame == &m_frame->tree().top()) {
-        bool triggeredByUserActivation = UserGestureIndicator::processingUserGesture();
         // 1. If this algorithm is triggered by user activation and A's active document's active sandboxing flag set has its sandboxed top-level navigation with user activation browsing context flag set, then abort these steps negatively.
-        if (triggeredByUserActivation && isSandboxed(SandboxTopNavigationByUserActivation)) {
+        if (isProcessingUserGestureForDocument && isSandboxed(SandboxTopNavigationByUserActivation)) {
             printNavigationErrorMessage(targetFrame, url(), "The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation-by-user-activation' flag is not set and navigation is not triggered by user activation."_s);
             return false;
         }
         // 2. Otherwise, If this algorithm is not triggered by user activation and A's active document's active sandboxing flag set has its sandboxed top-level navigation without user activation browsing context flag set, then abort these steps negatively.
-        if (!triggeredByUserActivation && isSandboxed(SandboxTopNavigation)) {
+        if (!isProcessingUserGestureForDocument && isSandboxed(SandboxTopNavigation)) {
             printNavigationErrorMessage(targetFrame, url(), "The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set."_s);
             return false;
         }

Modified: trunk/Source/WebCore/dom/UserGestureIndicator.cpp (270372 => 270373)


--- trunk/Source/WebCore/dom/UserGestureIndicator.cpp	2020-12-02 23:00:55 UTC (rev 270372)
+++ trunk/Source/WebCore/dom/UserGestureIndicator.cpp	2020-12-02 23:01:29 UTC (rev 270373)
@@ -30,6 +30,7 @@
 #include "Document.h"
 #include "Frame.h"
 #include "ResourceLoadObserver.h"
+#include "SecurityOrigin.h"
 #include <wtf/MainThread.h>
 #include <wtf/NeverDestroyed.h>
 #include <wtf/Optional.h>
@@ -43,6 +44,34 @@
     return token;
 }
 
+UserGestureToken::UserGestureToken(ProcessingUserGestureState state, UserGestureType gestureType, Document* document)
+    : m_state(state)
+    , m_gestureType(gestureType)
+{
+    if (!document || !processingUserGesture())
+        return;
+
+    // User gesture is valid for the document that received the user gesture, all of its ancestors
+    // as well as all same-origin documents on the page.
+    m_documentsImpactedByUserGesture.add(*document);
+
+    auto* documentFrame = document->frame();
+    if (!documentFrame)
+        return;
+
+    for (auto* ancestorFrame = documentFrame->tree().parent(); ancestorFrame; ancestorFrame = ancestorFrame->tree().parent()) {
+        if (auto* ancestorDocument = ancestorFrame->document())
+            m_documentsImpactedByUserGesture.add(ancestorDocument);
+    }
+
+    auto& documentOrigin = document->securityOrigin();
+    for (auto* frame = &documentFrame->tree().top(); frame; frame = frame->tree().traverseNext()) {
+        auto* frameDocument = frame->document();
+        if (frameDocument && documentOrigin.canAccess(frameDocument->securityOrigin()))
+            m_documentsImpactedByUserGesture.add(*frameDocument);
+    }
+}
+
 UserGestureToken::~UserGestureToken()
 {
     for (auto& observer : m_destructionObservers)
@@ -60,6 +89,11 @@
     maxIntervalForUserGestureForwardingForFetch = WTFMove(value);
 }
 
+bool UserGestureToken::isValidForDocument(Document& document) const
+{
+    return m_documentsImpactedByUserGesture.contains(document);
+}
+
 UserGestureIndicator::UserGestureIndicator(Optional<ProcessingUserGestureState> state, Document* document, UserGestureType gestureType, ProcessInteractionStyle processInteractionStyle)
     : m_previousToken { currentToken() }
 {
@@ -66,7 +100,7 @@
     ASSERT(isMainThread());
 
     if (state)
-        currentToken() = UserGestureToken::create(state.value(), gestureType);
+        currentToken() = UserGestureToken::create(state.value(), gestureType, document);
 
     if (document && currentToken()->processingUserGesture() && state) {
         document->updateLastHandledUserGestureTimestamp(currentToken()->startTime());
@@ -123,12 +157,15 @@
     return currentToken();
 }
 
-bool UserGestureIndicator::processingUserGesture()
+bool UserGestureIndicator::processingUserGesture(Document* document)
 {
     if (!isMainThread())
         return false;
 
-    return currentToken() ? currentToken()->processingUserGesture() : false;
+    if (!currentToken() || !currentToken()->processingUserGesture())
+        return false;
+
+    return !document || currentToken()->isValidForDocument(*document);
 }
 
 bool UserGestureIndicator::processingUserGestureForMedia()

Modified: trunk/Source/WebCore/dom/UserGestureIndicator.h (270372 => 270373)


--- trunk/Source/WebCore/dom/UserGestureIndicator.h	2020-12-02 23:00:55 UTC (rev 270372)
+++ trunk/Source/WebCore/dom/UserGestureIndicator.h	2020-12-02 23:01:29 UTC (rev 270373)
@@ -31,6 +31,7 @@
 #include <wtf/Noncopyable.h>
 #include <wtf/RefCounted.h>
 #include <wtf/Vector.h>
+#include <wtf/WeakHashSet.h>
 #include <wtf/WeakPtr.h>
 
 namespace WebCore {
@@ -51,9 +52,9 @@
     static const Seconds& maximumIntervalForUserGestureForwardingForFetch();
     WEBCORE_EXPORT static void setMaximumIntervalForUserGestureForwardingForFetchForTesting(Seconds);
 
-    static Ref<UserGestureToken> create(ProcessingUserGestureState state, UserGestureType gestureType)
+    static Ref<UserGestureToken> create(ProcessingUserGestureState state, UserGestureType gestureType, Document* document = nullptr)
     {
-        return adoptRef(*new UserGestureToken(state, gestureType));
+        return adoptRef(*new UserGestureToken(state, gestureType, document));
     }
 
     WEBCORE_EXPORT ~UserGestureToken();
@@ -101,16 +102,15 @@
 
     MonotonicTime startTime() const { return m_startTime; }
 
+    bool isValidForDocument(Document&) const;
+
 private:
-    UserGestureToken(ProcessingUserGestureState state, UserGestureType gestureType)
-        : m_state(state)
-        , m_gestureType(gestureType)
-    {
-    }
+    UserGestureToken(ProcessingUserGestureState, UserGestureType, Document*);
 
     ProcessingUserGestureState m_state = NotProcessingUserGesture;
     Vector<WTF::Function<void (UserGestureToken&)>> m_destructionObservers;
     UserGestureType m_gestureType;
+    WeakHashSet<Document> m_documentsImpactedByUserGesture;
     DOMPasteAccessPolicy m_domPasteAccessPolicy { DOMPasteAccessPolicy::NotRequestedYet };
     GestureScope m_scope { GestureScope::All };
     MonotonicTime m_startTime { MonotonicTime::now() };
@@ -123,7 +123,7 @@
 public:
     WEBCORE_EXPORT static RefPtr<UserGestureToken> currentUserGesture();
 
-    WEBCORE_EXPORT static bool processingUserGesture();
+    WEBCORE_EXPORT static bool processingUserGesture(Document* = nullptr);
     WEBCORE_EXPORT static bool processingUserGestureForMedia();
 
     // If a document is provided, its last known user gesture timestamp is updated.
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to