Skip to content

Commit

Permalink
Cherry-pick a0fa94d. rdar://problem/109364674
Browse files Browse the repository at this point in the history
    Restrict further top-frame navigations by a third-party iframe
    https://bugs.webkit.org/show_bug.cgi?id=256549
    rdar://108794051

    Reviewed by Geoffrey Garen.

    Restrict further top-frame navigations by a third-party iframe:
    - Block navigations to a different scheme
    - Block navigations that start off same-site but redirect to a different site

    * Source/WebCore/dom/Document.cpp:
    (WebCore::Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking):
    * Source/WebCore/dom/Document.h:
    * Source/WebCore/loader/DocumentLoader.cpp:
    (WebCore::DocumentLoader::willSendRequest):
    * Source/WebCore/loader/NavigationRequester.cpp:
    (WebCore::NavigationRequester::from):
    * Source/WebCore/loader/NavigationRequester.h:
    (WebCore::NavigationRequester::encode const):
    (WebCore::NavigationRequester::decode):

    Canonical link: https://commits.webkit.org/259548.752@safari-7615-branch

Canonical link: https://commits.webkit.org/245886.889@safari-7613.4.1.0-branch
  • Loading branch information
cdumez authored and rjepstein committed Jun 29, 2023
1 parent 72ad0cc commit 75be9e3
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 8 deletions.
7 changes: 6 additions & 1 deletion LayoutTests/fast/files/null-origin-string-expected.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
CONSOLE MESSAGE: Started reading...
PASS if no crash.

Test that using FileReader from a document with unique origin doesn't cause a crash.

If testing manually, please drop a file on an input above.

PASS if not crash.
8 changes: 7 additions & 1 deletion LayoutTests/fast/files/null-origin-string.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
reader.readAsText(file);
console.log('Started reading...');

top.location = 'data:text/html,<p>PASS if no crash.</p><script>testRunner.notifyDone()</scr' + 'ipt>';
top.postMessage('finish', '*');
}
</script>

Expand All @@ -25,10 +25,16 @@
testRunner.dumpAsText();
testRunner.waitUntilDone();
}
addEventListener('message', (e) => {
if (e.data == 'finish')
testRunner.notifyDone();
});

document.write('<iframe src="data:text/html,<input type=file id=file onchange=\'onInputFileChange()\'><script>' + document.getElementsByTagName("script")[0].innerText + 'runTest()</scr' + 'ipt>" style="left:0px;top:0px"></iframe>');
</script>

<p>Test that using FileReader from a document with unique origin doesn't cause a crash.</p>
<p>If testing manually, please drop a file on an input above.</p>
<p>PASS if not crash.</p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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-navigation-to-different-scheme-by-third-party-iframes.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page-different-scheme.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-navigation-to-different-scheme-by-third-party-iframes.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page-different-scheme.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 third-party iframe (same-site but different scheme)

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

Original file line number Diff line number Diff line change
@@ -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 third-party iframe (same-site but different scheme)");
jsTestIsAsync = true;
onload = () => {
setTimeout(() => {
document.getElementById('testFrame').src = "http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page-different-scheme.html";
setTimeout(() => {
testPassed("All navigations by subframes have been blocked");
finishJSTest();
}, 100);
}, 10);
}
</script>
<iframe src="http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page-different-scheme.html"></iframe>
<iframe id="testFrame"></iframe>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CONSOLE MESSAGE: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/block-top-level-navigation-via-redirect-by-third-party-iframes.html' from frame with URL 'http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page-via-redirect.html'. The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame.
Test blocking of suspicious top-level navigations by a third-party iframe (same-site but redirects to a different site)

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<body>
<script src="/js-test-resources/js-test.js"></script>
<script>
description("Test blocking of suspicious top-level navigations by a third-party iframe (same-site but redirects to a different site)");
jsTestIsAsync = true;
onload = () => {
setTimeout(() => {
document.getElementById('testFrame').src = "http://localhost:8000/security/resources/navigate-top-level-frame-to-failure-page-via-redirect.html";
setTimeout(() => {
testPassed("All navigations by subframes have been blocked");
finishJSTest();
}, 100);
}, 10);
}
</script>
<iframe id="testFrame"></iframe>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<body>
Success! The navigation was blocked
<script>
window.addEventListener("load", e => {
top.location = "https://127.0.0.1:8443/security/resources/should-not-have-loaded.html";
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<html>
<body>
Success! The navigation was blocked
<script>
window.addEventListener("load", e => {
// The initial navigation URL is same-site but it redirects to a different site.
top.location = "http://127.0.0.1:8000/resources/redirect.py?url=http://localhost:8000/security/resources/should-not-have-loaded.html";
});
</script>
</body>
</html>
9 changes: 6 additions & 3 deletions Source/WebCore/dom/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3699,10 +3699,13 @@ bool Document::isNavigationBlockedByThirdPartyIFrameRedirectBlocking(Frame& targ

// Only prevent cross-site navigations.
RefPtr targetDocument = targetFrame.document();
if (targetDocument && (targetDocument->securityOrigin().isSameOriginDomain(SecurityOrigin::create(destinationURL)) || areRegistrableDomainsEqual(targetDocument->url(), destinationURL)))
return false;
if (!targetDocument)
return true;

return true;
if (targetDocument->securityOrigin().protocol() != destinationURL.protocol())
return true;

return !(targetDocument->securityOrigin().isSameOriginDomain(SecurityOrigin::create(destinationURL)) || areRegistrableDomainsEqual(targetDocument->url(), destinationURL));
}

void Document::didRemoveAllPendingStylesheet()
Expand Down
3 changes: 2 additions & 1 deletion Source/WebCore/dom/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,8 @@ class Document
void addElementWithPendingUserAgentShadowTreeUpdate(Element&);
WEBCORE_EXPORT void removeElementWithPendingUserAgentShadowTreeUpdate(Element&);

bool isNavigationBlockedByThirdPartyIFrameRedirectBlocking(Frame& targetFrame, const URL& destinationURL);

protected:
enum ConstructionFlags { Synthesized = 1, NonRenderedPlaceholder = 1 << 1 };
WEBCORE_EXPORT Document(Frame*, const Settings&, const URL&, DocumentClasses = { }, unsigned constructionFlags = 0);
Expand Down Expand Up @@ -1779,7 +1781,6 @@ class Document
void didLoadResourceSynchronously(const URL&) final;

bool canNavigateInternal(Frame& targetFrame);
bool isNavigationBlockedByThirdPartyIFrameRedirectBlocking(Frame& targetFrame, const URL& destinationURL);

#if USE(QUICK_LOOK)
bool shouldEnforceQuickLookSandbox() const;
Expand Down
18 changes: 18 additions & 0 deletions Source/WebCore/loader/DocumentLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,24 @@ void DocumentLoader::willSendRequest(ResourceRequest&& newRequest, const Resourc
return completionHandler(WTFMove(newRequest));
}

if (auto requester = m_triggeringAction.requester(); requester && requester->documentIdentifier) {
if (RefPtr requestingDocument = Document::allDocumentsMap().get(requester->documentIdentifier); requestingDocument && requestingDocument->frame()) {
if (m_frame && requestingDocument->isNavigationBlockedByThirdPartyIFrameRedirectBlocking(*m_frame, newRequest.url())) {
DOCUMENTLOADER_RELEASE_LOG("willSendRequest: canceling - cross-site redirect of top frame triggered by third-party iframe");
if (m_frame->document()) {
auto message = makeString("Unsafe JavaScript attempt to initiate navigation for frame with URL '"
, m_frame->document()->url().string()
, "' from frame with URL '"
, requestingDocument->url().string()
, "'. The frame attempting navigation of the top-level window is cross-origin or untrusted and the user has never interacted with the frame.");
m_frame->document()->addConsoleMessage(MessageSource::Security, MessageLevel::Error, message);
}
cancelMainResourceLoad(frameLoader()->blockedError(newRequest));
return completionHandler(WTFMove(newRequest));
}
}
}

ASSERT(timing().startTime());
if (didReceiveRedirectResponse) {
// If the redirecting url is not allowed to display content from the target origin,
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/loader/NavigationRequester.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ NavigationRequester NavigationRequester::from(Document& document)
document.securityOrigin(),
document.topOrigin(),
document.crossOriginOpenerPolicy(),
document.identifier(),
createGlobalFrameIdentifier(document)
};
}
Expand Down
10 changes: 8 additions & 2 deletions Source/WebCore/loader/NavigationRequester.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct NavigationRequester {
Ref<SecurityOrigin> securityOrigin;
Ref<SecurityOrigin> topOrigin;
CrossOriginOpenerPolicy crossOriginOpenerPolicy;
ScriptExecutionContextIdentifier documentIdentifier;
std::optional<GlobalFrameIdentifier> globalFrameIdentifier;

template<class Encoder> void encode(Encoder&) const;
Expand All @@ -49,7 +50,7 @@ struct NavigationRequester {
template<class Encoder>
void NavigationRequester::encode(Encoder& encoder) const
{
encoder << url << securityOrigin.get() << topOrigin.get() << crossOriginOpenerPolicy << globalFrameIdentifier;
encoder << url << securityOrigin.get() << topOrigin.get() << crossOriginOpenerPolicy << documentIdentifier << globalFrameIdentifier;
}

template<class Decoder>
Expand All @@ -73,12 +74,17 @@ std::optional<NavigationRequester> NavigationRequester::decode(Decoder& decoder)
if (!crossOriginOpenerPolicy)
return std::nullopt;

std::optional<ScriptExecutionContextIdentifier> documentIdentifier;
decoder >> documentIdentifier;
if (!documentIdentifier)
return std::nullopt;

std::optional<std::optional<GlobalFrameIdentifier>> globalFrameIdentifier;
decoder >> globalFrameIdentifier;
if (!globalFrameIdentifier)
return std::nullopt;

return NavigationRequester { WTFMove(*url), securityOrigin.releaseNonNull(), topOrigin.releaseNonNull(), WTFMove(*crossOriginOpenerPolicy), *globalFrameIdentifier };
return NavigationRequester { WTFMove(*url), securityOrigin.releaseNonNull(), topOrigin.releaseNonNull(), WTFMove(*crossOriginOpenerPolicy), *documentIdentifier, *globalFrameIdentifier };
}

} // namespace WebCore

0 comments on commit 75be9e3

Please sign in to comment.