⚠ Archived content — this site is no longer maintained.   Current WebKit documentation is at docs.webkit.org.

Changeset 239742 in webkit


Ignore:
Timestamp:
Jan 8, 2019, 1:28:53 PM (8 years ago)
Author:
Chris Dumez
Message:

Prevent cross-site top-level navigations from third-party iframes
https://bugs.webkit.org/show_bug.cgi?id=193076
<rdar://problem/36074736>

Reviewed by Alex Christensen.

Source/WebCore:

Prevent cross-site top-level navigations from third-party iframes if the following conditions are met:

  1. Its tries to navigate the top-level page cross-site (different eTDL+1)
  2. The user has never interacted with the third-party iframe or any of its subframes

This experiment's intent is to block suspicious main-frame navigations by third-party content. The feature
is behind a runtime experimental feature flag, on by default.

Tests: http/tests/security/allow-top-level-navigations-by-third-party-iframes-to-same-origin.html

http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-previous-user-activation.html
http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-user-activation.html
http/tests/security/block-top-level-navigations-by-third-party-iframes.html

  • dom/Document.cpp:

(WebCore::printNavigationErrorMessage):
(WebCore::Document::canNavigate):
(WebCore::Document::canNavigateInternal):
(WebCore::Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking):

  • dom/Document.h:
  • dom/UserGestureIndicator.cpp:
  • page/DOMWindow.cpp:

(WebCore::DOMWindow::setLocation):

  • page/DOMWindow.h:
  • page/Frame.h:
  • page/Location.cpp:

(WebCore::Location::replace):
(WebCore::Location::setLocation):

  • page/Settings.yaml:

Source/WebKit:

Add experimental feature flag, on by default.

  • Shared/WebPreferences.yaml:

LayoutTests:

Add layout test coverage.

  • http/tests/security/allow-top-level-navigations-by-third-party-iframes-to-same-origin-expected.txt: Added.
  • http/tests/security/allow-top-level-navigations-by-third-party-iframes-to-same-origin.html: Added.
  • http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-previous-user-activation-expected.txt: Added.
  • http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-previous-user-activation.html: Added.
  • http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-user-activation-expected.txt: Added.
  • http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-user-activation.html: Added.
  • http/tests/security/block-top-level-navigations-by-third-party-iframes-expected.txt: Added.
  • http/tests/security/block-top-level-navigations-by-third-party-iframes.html: Added.
  • http/tests/security/resources/navigate-top-level-frame-to-failure-page.html: Added.
  • http/tests/security/resources/navigate-top-level-frame-to-success-page-same-origin.html: Added.
  • http/tests/security/resources/navigate-top-level-frame-to-success-page-with-previous-user-gesture.html: Added.
  • http/tests/security/resources/navigate-top-level-frame-to-success-page-with-user-gesture.html: Added.
  • http/tests/security/resources/should-have-loaded.html: Added.
  • http/tests/security/resources/should-not-have-loaded.html: Added.
Location:
trunk
Files:
14 added
17 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r239732 r239742  
     12019-01-08  Chris Dumez  <cdumez@apple.com>
     2
     3        Prevent cross-site top-level navigations from third-party iframes
     4        https://bugs.webkit.org/show_bug.cgi?id=193076
     5        <rdar://problem/36074736>
     6
     7        Reviewed by Alex Christensen.
     8
     9        Add layout test coverage.
     10
     11        * http/tests/security/allow-top-level-navigations-by-third-party-iframes-to-same-origin-expected.txt: Added.
     12        * http/tests/security/allow-top-level-navigations-by-third-party-iframes-to-same-origin.html: Added.
     13        * http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-previous-user-activation-expected.txt: Added.
     14        * http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-previous-user-activation.html: Added.
     15        * http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-user-activation-expected.txt: Added.
     16        * http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-user-activation.html: Added.
     17        * http/tests/security/block-top-level-navigations-by-third-party-iframes-expected.txt: Added.
     18        * http/tests/security/block-top-level-navigations-by-third-party-iframes.html: Added.
     19        * http/tests/security/resources/navigate-top-level-frame-to-failure-page.html: Added.
     20        * http/tests/security/resources/navigate-top-level-frame-to-success-page-same-origin.html: Added.
     21        * http/tests/security/resources/navigate-top-level-frame-to-success-page-with-previous-user-gesture.html: Added.
     22        * http/tests/security/resources/navigate-top-level-frame-to-success-page-with-user-gesture.html: Added.
     23        * http/tests/security/resources/should-have-loaded.html: Added.
     24        * http/tests/security/resources/should-not-have-loaded.html: Added.
     25
    1262019-01-08  Truitt Savell  <tsavell@apple.com>
    227
  • trunk/LayoutTests/http/tests/cookies/same-site/resources/click-hyperlink.php

    r230944 r239742  
    1414?>
    1515<a href="<?php echo $_GET['href']; ?>" <?php echo $targetAttribute; ?>>Click</a>
    16 <script>document.querySelector("a").click()</script>
     16<script>
     17internals.withUserGesture(() => {
     18    document.querySelector("a").click();
     19});
     20</script>
    1721</body>
    1822</html>
  • trunk/LayoutTests/http/tests/security/frameNavigation/resources/iframe-that-performs-parent-navigation.html

    r120174 r239742  
    1212        function performTest()
    1313        {
    14             parent.location = "http://localhost:8000/security/frameNavigation/resources/navigation-changed-iframe.html";
     14            internals.withUserGesture(() => {
     15                parent.location = "http://localhost:8000/security/frameNavigation/resources/navigation-changed-iframe.html";
     16            });
    1517        }
    1618    </script>
  • trunk/LayoutTests/http/tests/security/resources/xss-DENIED-window-open-parent-attacker.html

    r91152 r239742  
    11<script>
    2 open("javascript:alert('failed')", "_top");
    3 parent.postMessage("", "*");
     2internals.withUserGesture(() => {
     3    open("javascript:alert('failed')", "_top");
     4    parent.postMessage("", "*");
     5});
    46</script>
  • trunk/LayoutTests/http/tests/security/xss-DENIED-window-open-parent-expected.txt

    r178527 r239742  
    1 CONSOLE MESSAGE: line 2: Blocked a frame with origin "http://localhost:8080" from accessing a frame with origin "http://127.0.0.1:8000". Protocols, domains, and ports must match.
     1CONSOLE MESSAGE: line 3: Blocked a frame with origin "http://localhost:8080" from accessing a frame with origin "http://127.0.0.1:8000". Protocols, domains, and ports must match.
    22This test passes if there is no alert dialog.
    33
  • trunk/Source/WebCore/ChangeLog

    r239737 r239742  
     12019-01-08  Chris Dumez  <cdumez@apple.com>
     2
     3        Prevent cross-site top-level navigations from third-party iframes
     4        https://bugs.webkit.org/show_bug.cgi?id=193076
     5        <rdar://problem/36074736>
     6
     7        Reviewed by Alex Christensen.
     8
     9        Prevent cross-site top-level navigations from third-party iframes if the following conditions are met:
     10        1. Its tries to navigate the top-level page cross-site (different eTDL+1)
     11        2. The user has never interacted with the third-party iframe or any of its subframes
     12
     13        This experiment's intent is to block suspicious main-frame navigations by third-party content. The feature
     14        is behind a runtime experimental feature flag, on by default.
     15
     16        Tests: http/tests/security/allow-top-level-navigations-by-third-party-iframes-to-same-origin.html
     17               http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-previous-user-activation.html
     18               http/tests/security/allow-top-level-navigations-by-third-party-iframes-with-user-activation.html
     19               http/tests/security/block-top-level-navigations-by-third-party-iframes.html
     20
     21        * dom/Document.cpp:
     22        (WebCore::printNavigationErrorMessage):
     23        (WebCore::Document::canNavigate):
     24        (WebCore::Document::canNavigateInternal):
     25        (WebCore::Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking):
     26        * dom/Document.h:
     27        * dom/UserGestureIndicator.cpp:
     28        * page/DOMWindow.cpp:
     29        (WebCore::DOMWindow::setLocation):
     30        * page/DOMWindow.h:
     31        * page/Frame.h:
     32        * page/Location.cpp:
     33        (WebCore::Location::replace):
     34        (WebCore::Location::setLocation):
     35        * page/Settings.yaml:
     36
    1372019-01-08  Alex Christensen  <achristensen@webkit.org>
    238
  • trunk/Source/WebCore/dom/Document.cpp

    r239640 r239742  
    457457}
    458458
    459 static void printNavigationErrorMessage(Frame* frame, const URL& activeURL, const char* reason)
    460 {
    461     String message = "Unsafe JavaScript attempt to initiate navigation for frame with URL '" + frame->document()->url().string() + "' from frame with URL '" + activeURL.string() + "'. " + reason + "\n";
     459static void printNavigationErrorMessage(Frame& frame, const URL& activeURL, const char* reason)
     460{
     461    String message = "Unsafe JavaScript attempt to initiate navigation for frame with URL '" + frame.document()->url().string() + "' from frame with URL '" + activeURL.string() + "'. " + reason + "\n";
    462462
    463463    // FIXME: should we print to the console of the document performing the navigation instead?
    464     frame->document()->domWindow()->printErrorMessage(message);
     464    frame.document()->domWindow()->printErrorMessage(message);
    465465}
    466466
     
    33383338}
    33393339   
    3340 bool Document::canNavigate(Frame* targetFrame)
     3340bool Document::canNavigate(Frame* targetFrame, const URL& destinationURL)
    33413341{
    33423342    if (!m_frame)
     
    33493349        return true;
    33503350
     3351    if (!canNavigateInternal(*targetFrame))
     3352        return false;
     3353
     3354    if (isNavigationBlockedByThirdPartyIFrameRedirectBlocking(*targetFrame, destinationURL)) {
     3355        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);
     3356        return false;
     3357    }
     3358
     3359    return true;
     3360}
     3361
     3362bool Document::canNavigateInternal(Frame& targetFrame)
     3363{
     3364    ASSERT(m_frame);
     3365
    33513366    // Cases (i), (ii) and (iii) pass the tests from the specifications but might not pass the "security origin" tests.
    33523367
    33533368    // i. A frame can navigate its top ancestor when its 'allow-top-navigation' flag is set (sometimes known as 'frame-busting').
    3354     if (!isSandboxed(SandboxTopNavigation) && targetFrame == &m_frame->tree().top())
     3369    if (!isSandboxed(SandboxTopNavigation) && &targetFrame == &m_frame->tree().top())
    33553370        return true;
    33563371
    33573372    // 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.
    3358     if (!isSandboxed(SandboxTopNavigationByUserActivation) && UserGestureIndicator::processingUserGesture() && targetFrame == &m_frame->tree().top())
     3373    if (!isSandboxed(SandboxTopNavigationByUserActivation) && UserGestureIndicator::processingUserGesture() && &targetFrame == &m_frame->tree().top())
    33593374        return true;
    33603375
    33613376    // iii. A sandboxed frame can always navigate its descendants.
    3362     if (isSandboxed(SandboxNavigation) && targetFrame->tree().isDescendantOf(m_frame))
     3377    if (isSandboxed(SandboxNavigation) && targetFrame.tree().isDescendantOf(m_frame))
    33633378        return true;
    33643379
     
    33663381    // 1. If A is not the same browsing context as B, and A is not one of the ancestor browsing contexts of B, and B is not a top-level browsing context, and A's active document's active sandboxing
    33673382    // flag set has its sandboxed navigation browsing context flag set, then abort these steps negatively.
    3368     if (m_frame != targetFrame && isSandboxed(SandboxNavigation) && targetFrame->tree().parent() && !targetFrame->tree().isDescendantOf(m_frame)) {
     3383    if (m_frame != &targetFrame && isSandboxed(SandboxNavigation) && targetFrame.tree().parent() && !targetFrame.tree().isDescendantOf(m_frame)) {
    33693384        printNavigationErrorMessage(targetFrame, url(), "The frame attempting navigation is sandboxed, and is therefore disallowed from navigating its ancestors."_s);
    33703385        return false;
     
    33723387
    33733388    // 2. Otherwise, if B is a top-level browsing context, and is one of the ancestor browsing contexts of A, then:
    3374     if (m_frame != targetFrame && targetFrame == &m_frame->tree().top()) {
     3389    if (m_frame != &targetFrame && &targetFrame == &m_frame->tree().top()) {
    33753390        bool triggeredByUserActivation = UserGestureIndicator::processingUserGesture();
    33763391        // 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.
     
    33883403    // 3. Otherwise, if B is a top-level browsing context, and is neither A nor one of the ancestor browsing contexts of A, and A's Document's active sandboxing flag set has its
    33893404    // sandboxed navigation browsing context flag set, and A is not the one permitted sandboxed navigator of B, then abort these steps negatively.
    3390     if (!targetFrame->tree().parent() && m_frame != targetFrame && targetFrame != &m_frame->tree().top() && isSandboxed(SandboxNavigation) && targetFrame->loader().opener() != m_frame) {
     3405    if (!targetFrame.tree().parent() && m_frame != &targetFrame && &targetFrame != &m_frame->tree().top() && isSandboxed(SandboxNavigation) && targetFrame.loader().opener() != m_frame) {
    33913406        printNavigationErrorMessage(targetFrame, url(), "The frame attempting navigation is sandboxed, and is not allowed to navigate this popup."_s);
    33923407        return false;
     
    34023417    // See http://www.adambarth.com/papers/2008/barth-jackson-mitchell.pdf for
    34033418    // historical information about this security check.
    3404     if (canAccessAncestor(securityOrigin(), targetFrame))
     3419    if (canAccessAncestor(securityOrigin(), &targetFrame))
    34053420        return true;
    34063421
     
    34163431    // and/or "parent" relation). Requiring some sort of relation prevents a
    34173432    // document from navigating arbitrary, unrelated top-level frames.
    3418     if (!targetFrame->tree().parent()) {
    3419         if (targetFrame == m_frame->loader().opener())
     3433    if (!targetFrame.tree().parent()) {
     3434        if (&targetFrame == m_frame->loader().opener())
    34203435            return true;
    34213436
    3422         if (canAccessAncestor(securityOrigin(), targetFrame->loader().opener()))
     3437        if (canAccessAncestor(securityOrigin(), targetFrame.loader().opener()))
    34233438            return true;
    34243439    }
     
    34263441    printNavigationErrorMessage(targetFrame, url(), "The frame attempting navigation is neither same-origin with the target, nor is it the target's parent or opener.");
    34273442    return false;
     3443}
     3444
     3445// Prevent cross-site top-level redirects from third-party iframes unless the user has ever interacted with the frame.
     3446bool Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking(Frame& targetFrame, const URL& destinationURL)
     3447{
     3448    if (!settings().thirdPartyIframeRedirectBlockingEnabled())
     3449        return false;
     3450
     3451    // Only prevent top frame navigations by subframes.
     3452    if (m_frame == &targetFrame || &targetFrame != &m_frame->tree().top())
     3453        return false;
     3454
     3455    // Only prevent navigations by subframes that the user has not interacted with.
     3456    if (m_frame->hasHadUserInteraction())
     3457        return false;
     3458
     3459    // Only prevent navigations by unsandboxed iframes. Such navigations by unsandboxed iframes would have already been blocked unless
     3460    // "allow-top-navigation" / "allow-top-navigation-by-user-activation" was explicitly specified.
     3461    if (sandboxFlags() != SandboxNone)
     3462        return false;
     3463
     3464    // Only prevent navigations by third-party iframes.
     3465    if (canAccessAncestor(securityOrigin(), &targetFrame))
     3466        return false;
     3467
     3468    // Only prevent cross-site navigations.
     3469    auto* targetDocument = targetFrame.document();
     3470    if (targetDocument && (targetDocument->securityOrigin().canAccess(SecurityOrigin::create(destinationURL)) || registrableDomainsAreEqual(targetDocument->url(), destinationURL)))
     3471        return false;
     3472
     3473    return true;
    34283474}
    34293475
  • trunk/Source/WebCore/dom/Document.h

    r239640 r239742  
    707707    SocketProvider* socketProvider() final;
    708708
    709     bool canNavigate(Frame* targetFrame);
     709    bool canNavigate(Frame* targetFrame, const URL& destinationURL = URL());
    710710
    711711    bool usesStyleBasedEditability() const;
     
    16451645    void checkAppearanceDependentPictures();
    16461646
     1647    bool canNavigateInternal(Frame& targetFrame);
     1648    bool isNavigationBlockedByThirdPartyIFrameRedirectBlocking(Frame& targetFrame, const URL& destinationURL);
     1649
    16471650#if ENABLE(INTERSECTION_OBSERVER)
    16481651    void notifyIntersectionObserversTimerFired();
  • trunk/Source/WebCore/dom/UserGestureIndicator.cpp

    r239427 r239742  
    2828
    2929#include "Document.h"
     30#include "Frame.h"
    3031#include "ResourceLoadObserver.h"
    3132#include <wtf/MainThread.h>
     
    6061            ResourceLoadObserver::shared().logUserInteractionWithReducedTimeResolution(document->topDocument());
    6162        document->topDocument().setUserDidInteractWithPage(true);
     63        if (auto* frame = document->frame()) {
     64            if (!frame->hasHadUserInteraction()) {
     65                for (; frame; frame = frame->tree().parent())
     66                    frame->setHasHadUserInteraction();
     67            }
     68        }
    6269    }
    6370}
  • trunk/Source/WebCore/page/DOMWindow.cpp

    r239639 r239742  
    21042104}
    21052105
    2106 void DOMWindow::setLocation(DOMWindow& activeWindow, DOMWindow& firstWindow, const String& urlString, SetLocationLocking locking)
     2106void DOMWindow::setLocation(DOMWindow& activeWindow, const URL& completedURL, SetLocationLocking locking)
    21072107{
    21082108    if (!isCurrentlyDisplayedInFrame())
     
    21142114
    21152115    auto* frame = this->frame();
    2116     if (!activeDocument->canNavigate(frame))
    2117         return;
    2118 
    2119     Frame* firstFrame = firstWindow.frame();
    2120     if (!firstFrame)
    2121         return;
    2122 
    2123     URL completedURL = firstFrame->document()->completeURL(urlString);
    2124     if (completedURL.isNull())
     2116    if (!activeDocument->canNavigate(frame, completedURL))
    21252117        return;
    21262118
  • trunk/Source/WebCore/page/DOMWindow.h

    r239427 r239742  
    146146
    147147    Location& location();
    148     void setLocation(DOMWindow& activeWindow, DOMWindow& firstWindow, const String& location, SetLocationLocking = LockHistoryBasedOnGestureState);
     148    void setLocation(DOMWindow& activeWindow, const URL& completedURL, SetLocationLocking = LockHistoryBasedOnGestureState);
    149149
    150150    DOMSelection* getSelection();
  • trunk/Source/WebCore/page/Frame.h

    r238771 r239742  
    173173    bool documentIsBeingReplaced() const { return m_documentIsBeingReplaced; }
    174174
     175    bool hasHadUserInteraction() const { return m_hasHadUserInteraction; }
     176    void setHasHadUserInteraction() { m_hasHadUserInteraction = true; }
     177
    175178// ======== All public functions below this point are candidates to move out of Frame into another class. ========
    176179
     
    349352    unsigned m_navigationDisableCount { 0 };
    350353    unsigned m_selfOnlyRefCount { 0 };
     354    bool m_hasHadUserInteraction { false };
    351355
    352356protected:
  • trunk/Source/WebCore/page/Location.cpp

    r238771 r239742  
    226226}
    227227
    228 void Location::replace(DOMWindow& activeWindow, DOMWindow& firstWindow, const String& url)
     228void Location::replace(DOMWindow& activeWindow, DOMWindow& firstWindow, const String& urlString)
    229229{
    230230    auto* frame = this->frame();
     
    233233    ASSERT(frame->document());
    234234    ASSERT(frame->document()->domWindow());
     235
     236    Frame* firstFrame = firstWindow.frame();
     237    if (!firstFrame || !firstFrame->document())
     238        return;
     239
     240    URL completedURL = firstFrame->document()->completeURL(urlString);
     241    // FIXME: The specification says to throw a SyntaxError if the URL is not valid.
     242    if (completedURL.isNull())
     243        return;
     244
    235245    // We call DOMWindow::setLocation directly here because replace() always operates on the current frame.
    236     frame->document()->domWindow()->setLocation(activeWindow, firstWindow, url, LockHistoryAndBackForwardList);
     246    frame->document()->domWindow()->setLocation(activeWindow, completedURL, LockHistoryAndBackForwardList);
    237247}
    238248
     
    265275}
    266276
    267 ExceptionOr<void> Location::setLocation(DOMWindow& activeWindow, DOMWindow& firstWindow, const String& url)
     277ExceptionOr<void> Location::setLocation(DOMWindow& activeWindow, DOMWindow& firstWindow, const String& urlString)
    268278{
    269279    auto* frame = this->frame();
    270280    ASSERT(frame);
    271     if (!activeWindow.document()->canNavigate(frame))
     281
     282    Frame* firstFrame = firstWindow.frame();
     283    if (!firstFrame || !firstFrame->document())
     284        return { };
     285
     286    URL completedURL = firstFrame->document()->completeURL(urlString);
     287    // FIXME: The specification says to throw a SyntaxError if the URL is not valid.
     288    if (completedURL.isNull())
     289        return { };
     290
     291    if (!activeWindow.document()->canNavigate(frame, completedURL))
    272292        return Exception { SecurityError };
     293
    273294    ASSERT(frame->document());
    274295    ASSERT(frame->document()->domWindow());
    275     frame->document()->domWindow()->setLocation(activeWindow, firstWindow, url);
     296    frame->document()->domWindow()->setLocation(activeWindow, completedURL);
    276297    return { };
    277298}
  • trunk/Source/WebCore/page/Settings.yaml

    r239474 r239742  
    333333  initial: false
    334334
     335thirdPartyIframeRedirectBlockingEnabled:
     336  initial: true
     337
    335338cookieEnabled:
    336339  initial: true
  • trunk/Source/WebCore/platform/network/ResourceRequestBase.h

    r239427 r239742  
    261261inline String toRegistrableDomain(const URL& a)
    262262{
    263     return ResourceRequestBase::partitionName(a.host().toString());
     263    auto host = a.host().toString();
     264    auto registrableDomain = ResourceRequestBase::partitionName(host);
     265    // Fall back to the host if we cannot determine the registrable domain.
     266    return registrableDomain.isEmpty() ? host : registrableDomain;
    264267}
    265268
  • trunk/Source/WebKit/ChangeLog

    r239741 r239742  
     12019-01-08  Chris Dumez  <cdumez@apple.com>
     2
     3        Prevent cross-site top-level navigations from third-party iframes
     4        https://bugs.webkit.org/show_bug.cgi?id=193076
     5        <rdar://problem/36074736>
     6
     7        Reviewed by Alex Christensen.
     8
     9        Add experimental feature flag, on by default.
     10
     11        * Shared/WebPreferences.yaml:
     12
    1132019-01-08  Alex Christensen  <achristensen@webkit.org>
    214
  • trunk/Source/WebKit/Shared/WebPreferences.yaml

    r239704 r239742  
    4141   humanReadableName: "Automatic HTTPS upgrade"
    4242   humanReadableDescription: "Automatic HTTPS upgrade for known supported sites"
     43   category: experimental
     44
     45ThirdPartyIframeRedirectBlockingEnabled:
     46   type: bool
     47   defaultValue: true
     48   humanReadableName: "Block top-level redirects by third-party iframes"
     49   humanReadableDescription: "Block top-level redirects by third-party iframes"
    4350   category: experimental
    4451
Note: See TracChangeset for help on using the changeset viewer.