Commit b9aa142c authored by japhet's avatar japhet Committed by Commit bot

Block framebusts without a user gesture

Intent to deprecate and remove: https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/Xi8-y4ySjA4

BUG=624061

Review-Url: https://codereview.chromium.org/2092293002
Cr-Commit-Position: refs/heads/master@{#417102}
parent 717f854a
...@@ -15211,6 +15211,12 @@ Please check your email at <ph name="ACCOUNT_EMAIL">$2<ex>jane.doe@gmail.com</ex ...@@ -15211,6 +15211,12 @@ Please check your email at <ph name="ACCOUNT_EMAIL">$2<ex>jane.doe@gmail.com</ex
<message name="IDS_FLAGS_FONT_CACHE_SCALING_DESCRIPTION" desc="Description for the flag to enable FontCache scaling"> <message name="IDS_FLAGS_FONT_CACHE_SCALING_DESCRIPTION" desc="Description for the flag to enable FontCache scaling">
Reuse a cached font in the renderer to serve different sizes of font for faster layout. Reuse a cached font in the renderer to serve different sizes of font for faster layout.
</message> </message>
<message name="IDS_FLAGS_FRAMEBUSTING_NAME" desc="Title for the flag to enable Framebusting restrictions">
Framebusting requires same-origin or a user gesture
</message>
<message name="IDS_FLAGS_FRAMEBUSTING_DESCRIPTION" desc="Description for the flag that prevents an iframe (typically an ad) from navigating the top level browsing context (the whole tab) unless the top and the iframe are part of the same website or the the navigation is requested in response to a user action (e.g., the user clicked on the tab)">
Don't permit an iframe to navigate the top level browsing context unless they are same-origin or the iframe is processing a user gesture.
</message>
<if expr="is_macosx"> <if expr="is_macosx">
<message name="IDS_ANNOUNCEMENT_COMPLETION_AVAILABLE_MAC" desc="Text to be <message name="IDS_ANNOUNCEMENT_COMPLETION_AVAILABLE_MAC" desc="Text to be
read aloud to screenreader users to announce that a completion is available."> read aloud to screenreader users to announce that a completion is available.">
......
...@@ -2008,6 +2008,9 @@ const FeatureEntry kFeatureEntries[] = { ...@@ -2008,6 +2008,9 @@ const FeatureEntry kFeatureEntries[] = {
{"enable-font-cache-scaling", IDS_FLAGS_FONT_CACHE_SCALING_NAME, {"enable-font-cache-scaling", IDS_FLAGS_FONT_CACHE_SCALING_NAME,
IDS_FLAGS_FONT_CACHE_SCALING_DESCRIPTION, kOsAll, IDS_FLAGS_FONT_CACHE_SCALING_DESCRIPTION, kOsAll,
FEATURE_VALUE_TYPE(features::kFontCacheScaling)}, FEATURE_VALUE_TYPE(features::kFontCacheScaling)},
{"enable-framebusting-needs-sameorigin-or-usergesture",
IDS_FLAGS_FRAMEBUSTING_NAME, IDS_FLAGS_FRAMEBUSTING_DESCRIPTION, kOsAll,
FEATURE_VALUE_TYPE(features::kFramebustingNeedsSameOriginOrUserGesture)},
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
{"enable-vr-shell", IDS_FLAGS_ENABLE_VR_SHELL_NAME, {"enable-vr-shell", IDS_FLAGS_ENABLE_VR_SHELL_NAME,
IDS_FLAGS_ENABLE_VR_SHELL_DESCRIPTION, kOsAndroid, IDS_FLAGS_ENABLE_VR_SHELL_DESCRIPTION, kOsAndroid,
......
...@@ -262,6 +262,11 @@ void SetRuntimeFeaturesDefaultsAndUpdateFromArgs( ...@@ -262,6 +262,11 @@ void SetRuntimeFeaturesDefaultsAndUpdateFromArgs(
"FontCacheScaling", "FontCacheScaling",
base::FeatureList::IsEnabled(features::kFontCacheScaling)); base::FeatureList::IsEnabled(features::kFontCacheScaling));
WebRuntimeFeatures::enableFeatureFromString(
"FramebustingNeedsSameOriginOrUserGesture",
base::FeatureList::IsEnabled(
features::kFramebustingNeedsSameOriginOrUserGesture));
if (base::FeatureList::IsEnabled(features::kParseHTMLOnMainThread)) if (base::FeatureList::IsEnabled(features::kParseHTMLOnMainThread))
WebRuntimeFeatures::enableFeatureFromString("ParseHTMLOnMainThread", true); WebRuntimeFeatures::enableFeatureFromString("ParseHTMLOnMainThread", true);
......
...@@ -45,6 +45,13 @@ const base::Feature kFeaturePolicy{"FeaturePolicy", ...@@ -45,6 +45,13 @@ const base::Feature kFeaturePolicy{"FeaturePolicy",
const base::Feature kFontCacheScaling{"FontCacheScaling", const base::Feature kFontCacheScaling{"FontCacheScaling",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
// Enables a security restriction on iframes navigating their top frame.
// When enabled, the navigation will only be permitted if the iframe is
// same-origin to the top frame, or if a user gesture is being processed.
const base::Feature kFramebustingNeedsSameOriginOrUserGesture{
"FramebustingNeedsSameOriginOrUserGesture",
base::FEATURE_ENABLED_BY_DEFAULT};
// Enables extended Gamepad API features like motion tracking and haptics. // Enables extended Gamepad API features like motion tracking and haptics.
const base::Feature kGamepadExtensions{"GamepadExtensions", const base::Feature kGamepadExtensions{"GamepadExtensions",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
......
...@@ -23,6 +23,8 @@ CONTENT_EXPORT extern const base::Feature kDefaultEnableGpuRasterization; ...@@ -23,6 +23,8 @@ CONTENT_EXPORT extern const base::Feature kDefaultEnableGpuRasterization;
CONTENT_EXPORT extern const base::Feature kDocumentWriteEvaluator; CONTENT_EXPORT extern const base::Feature kDocumentWriteEvaluator;
CONTENT_EXPORT extern const base::Feature kFeaturePolicy; CONTENT_EXPORT extern const base::Feature kFeaturePolicy;
CONTENT_EXPORT extern const base::Feature kFontCacheScaling; CONTENT_EXPORT extern const base::Feature kFontCacheScaling;
CONTENT_EXPORT extern const base::Feature
kFramebustingNeedsSameOriginOrUserGesture;
CONTENT_EXPORT extern const base::Feature kGamepadExtensions; CONTENT_EXPORT extern const base::Feature kGamepadExtensions;
CONTENT_EXPORT extern const base::Feature kMediaDocumentDownloadButton; CONTENT_EXPORT extern const base::Feature kMediaDocumentDownloadButton;
CONTENT_EXPORT extern const base::Feature kNewMediaPlaybackUi; CONTENT_EXPORT extern const base::Feature kNewMediaPlaybackUi;
......
...@@ -11,23 +11,22 @@ if (window.testRunner) { ...@@ -11,23 +11,22 @@ if (window.testRunner) {
testRunner.waitUntilDone(); testRunner.waitUntilDone();
} }
var t = setInterval(function(){ function blobSent(event) {
if (!location.hash) var blobURL = event.data.blobURL;
return;
var blobURL = location.hash.substr(1);
if (blobURL == "null") if (blobURL == "null")
log("FAIL: no blob URL is created"); log("FAIL: no blob URL is created");
else else
log("PASS: created blob URL"); log("PASS: created blob URL");
clearInterval(t);
if (window.testRunner) if (window.testRunner)
testRunner.notifyDone(); testRunner.notifyDone();
}, 100) };
window.addEventListener("message", blobSent, false);
</script> </script>
</head> </head>
<body> <body>
<p> Test case for checking blob URL can be created from the code running from data URI</p> <p> Test case for checking blob URL can be created from the code running from data URI</p>
<pre id='console'></pre> <pre id='console'></pre>
<iframe src="data:text/html,%3Cscript%3Evar%20bb%20%3D%20new%20Blob%28%5B%22Foo%22%5D%2C%20%7Btype%3A%22text%2Fhtml%22%7D%29%3Btop.location%3D%27http%3A%2F%2F127.0.0.1%3A8000%2Ffileapi%2Fcreate-blob-url-from-data-url.html%23%27%20%2B%20window.URL.createObjectURL%28bb%29%3B%3C%2Fscript%3E"></iframe> <iframe src="data:text/html,<body><script>var bb = new Blob(['Foo'], {type:'text/html'}); top.postMessage({blobURL: window.URL.createObjectURL(bb)}, '*');</script></body>"></iframe>
</body> </body>
</html> </html>
...@@ -4,19 +4,29 @@ ...@@ -4,19 +4,29 @@
function loaded() function loaded()
{ {
document.getElementsByTagName('h4')[0].innerHTML = document.domain; document.getElementsByTagName('h4')[0].innerHTML = document.domain;
// Allow the user to click the button during manuel runs. }
if (window.testRunner)
performTest(); function startTest(event)
{
// A manual click should navigate.
if (window.eventSender) {
var button = document.getElementById("b");
eventSender.mouseMoveTo(button.offsetLeft + event.data.x + 2, button.offsetTop + event.data.y + 2);
eventSender.mouseDown();
eventSender.mouseUp();
}
} }
function performTest() function performTest()
{ {
parent.location = "http://localhost:8000/security/frameNavigation/resources/navigation-changed-iframe.html"; parent.location = "http://localhost:8000/security/frameNavigation/resources/navigation-changed-iframe.html";
} }
window.addEventListener("message", startTest, false);
</script> </script>
</head> </head>
<body onload="loaded();"> <body onload="loaded();">
<h4>DOMAIN</h4> <h4>DOMAIN</h4>
<button onclick="performTest();">Perform Test</button> <button id="b" onclick="performTest();">Perform Test</button>
</body> </body>
</html> </html>
<html>
<body>
The navigation should fail and this iframe should be blocked. This text shouldn't appear.
<script>
window.onload = function()
{
top.location = "http://localhost:8000/security/frameNavigation/resources/navigation-changed-iframe.html";
}
</script>
</body>
</html>
...@@ -12,12 +12,17 @@ ...@@ -12,12 +12,17 @@
function loaded() function loaded()
{ {
document.getElementsByTagName('h4')[0].innerHTML = document.domain; document.getElementsByTagName('h4')[0].innerHTML = document.domain;
var iframe = document.getElementById("i");
// The iframe uses eventSender to emulate a user navigatation, which requires absolute coordinates.
// Because the iframe is cross-origin, it can't get the offsets itself, so leak them.
frames[0].postMessage({x: iframe.offsetLeft, y: iframe.offsetTop}, "*");
} }
</script> </script>
</head> </head>
<body onload="loaded();"> <body onload="loaded();">
<p>This tests that documents can navigate the location of any of it's parent-frames regardless of domain.</p> <p>This tests that documents can navigate the location of any of it's parent-frames regardless of domain, if a
user gesture is present.</p>
<h4>DOMAIN</h4> <h4>DOMAIN</h4>
<iframe src="http://localhost:8000/security/frameNavigation/resources/iframe-that-performs-parent-navigation.html"></iframe> <iframe id="i" src="http://localhost:8000/security/frameNavigation/resources/iframe-that-performs-parent-navigation.html"></iframe>
</body> </body>
</html> </html>
CONSOLE ERROR: line 7: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/frameNavigation/xss-DENIED-top-navigation-without-user-gesture.html' from frame with URL 'http://localhost:8000/security/frameNavigation/resources/iframe-that-performs-top-navigation-without-user-gesture.html'. The frame attempting navigation is targeting its top-level window, but is neither same-origin with its target nor is it processing a user gesture. See https://www.chromestatus.com/features/5851021045661696.
--------
Frame: '<!--framePath //<!--frame0-->-->'
--------
<html>
<head>
<script>
if (window.testRunner) {
testRunner.dumpAsText();
testRunner.dumpChildFramesAsText();
}
</script>
</head>
<body>
<iframe src="http://localhost:8000/security/frameNavigation/resources/iframe-that-performs-top-navigation-without-user-gesture.html"></iframe>
</body>
</html>
CONSOLE ERROR: 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. CONSOLE ERROR: line 2: Unsafe JavaScript attempt to initiate navigation for frame with URL 'http://127.0.0.1:8000/security/xss-DENIED-window-open-parent.html' from frame with URL 'http://localhost:8080/security/resources/xss-DENIED-window-open-parent-attacker.html'. The frame attempting navigation is targeting its top-level window, but is neither same-origin with its target nor is it processing a user gesture. See https://www.chromestatus.com/features/5851021045661696.
This test passes if there is no alert dialog. This test passes if there is no alert dialog.
...@@ -42,6 +42,7 @@ ...@@ -42,6 +42,7 @@
#include "core/layout/LayoutPart.h" #include "core/layout/LayoutPart.h"
#include "core/loader/EmptyClients.h" #include "core/loader/EmptyClients.h"
#include "core/loader/FrameLoaderClient.h" #include "core/loader/FrameLoaderClient.h"
#include "core/loader/NavigationScheduler.h"
#include "core/page/FocusController.h" #include "core/page/FocusController.h"
#include "core/page/Page.h" #include "core/page/Page.h"
#include "platform/Histogram.h" #include "platform/Histogram.h"
...@@ -168,19 +169,28 @@ bool Frame::canNavigate(const Frame& targetFrame) ...@@ -168,19 +169,28 @@ bool Frame::canNavigate(const Frame& targetFrame)
String errorReason; String errorReason;
bool isAllowedNavigation = canNavigateWithoutFramebusting(targetFrame, errorReason); bool isAllowedNavigation = canNavigateWithoutFramebusting(targetFrame, errorReason);
// Frame-busting is generally allowed, but blocked for sandboxed frames lacking the 'allow-top-navigation' flag.
if (targetFrame != this && !securityContext()->isSandboxed(SandboxTopNavigation) && targetFrame == tree().top()) { if (targetFrame != this && !securityContext()->isSandboxed(SandboxTopNavigation) && targetFrame == tree().top()) {
DEFINE_STATIC_LOCAL(EnumerationHistogram, framebustHistogram, ("WebCore.Framebust", 4)); DEFINE_STATIC_LOCAL(EnumerationHistogram, framebustHistogram, ("WebCore.Framebust", 4));
const unsigned userGestureBit = 0x1; const unsigned userGestureBit = 0x1;
const unsigned allowedBit = 0x2; const unsigned allowedBit = 0x2;
unsigned framebustParams = 0; unsigned framebustParams = 0;
UseCounter::count(&targetFrame, UseCounter::TopNavigationFromSubFrame); UseCounter::count(&targetFrame, UseCounter::TopNavigationFromSubFrame);
if (UserGestureIndicator::processingUserGesture()) bool hasUserGesture = UserGestureIndicator::processingUserGesture();
if (hasUserGesture)
framebustParams |= userGestureBit; framebustParams |= userGestureBit;
if (isAllowedNavigation) if (isAllowedNavigation)
framebustParams |= allowedBit; framebustParams |= allowedBit;
framebustHistogram.count(framebustParams); framebustHistogram.count(framebustParams);
return true; // Frame-busting used to be generally allowed in most situations, but may now blocked if there is no user gesture.
if (!RuntimeEnabledFeatures::framebustingNeedsSameOriginOrUserGestureEnabled())
return true;
if (hasUserGesture || isAllowedNavigation)
return true;
errorReason = "The frame attempting navigation is targeting its top-level window, but is neither same-origin with its target nor is it processing a user gesture. See https://www.chromestatus.com/features/5851021045661696.";
printNavigationErrorMessage(targetFrame, errorReason.latin1().data());
if (isLocalFrame())
toLocalFrame(this)->navigationScheduler().schedulePageBlock(toLocalFrame(this)->document());
return false;
} }
if (!isAllowedNavigation && !errorReason.isNull()) if (!isAllowedNavigation && !errorReason.isNull())
printNavigationErrorMessage(targetFrame, errorReason.latin1().data()); printNavigationErrorMessage(targetFrame, errorReason.latin1().data());
...@@ -198,6 +208,10 @@ bool Frame::canNavigateWithoutFramebusting(const Frame& targetFrame, String& rea ...@@ -198,6 +208,10 @@ bool Frame::canNavigateWithoutFramebusting(const Frame& targetFrame, String& rea
if (targetFrame == targetFrame.tree().top() && targetFrame.tree().top() != tree().top() && !securityContext()->isSandboxed(SandboxPropagatesToAuxiliaryBrowsingContexts)) if (targetFrame == targetFrame.tree().top() && targetFrame.tree().top() != tree().top() && !securityContext()->isSandboxed(SandboxPropagatesToAuxiliaryBrowsingContexts))
return true; return true;
// Top navigation can be opted-in.
if (!securityContext()->isSandboxed(SandboxTopNavigation) && targetFrame == tree().top())
return true;
// Otherwise, block the navigation. // Otherwise, block the navigation.
if (securityContext()->isSandboxed(SandboxTopNavigation) && targetFrame == tree().top()) if (securityContext()->isSandboxed(SandboxTopNavigation) && targetFrame == tree().top())
reason = "The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set."; reason = "The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set.";
......
...@@ -94,6 +94,7 @@ FeaturePolicy status=experimental ...@@ -94,6 +94,7 @@ FeaturePolicy status=experimental
FileAPIBlobClose status=experimental FileAPIBlobClose status=experimental
FileSystem status=stable FileSystem status=stable
ForeignFetch status=experimental, origin_trial_feature_name=ForeignFetch ForeignFetch status=experimental, origin_trial_feature_name=ForeignFetch
FramebustingNeedsSameOriginOrUserGesture status=stable
FullscreenUnprefixed status=test FullscreenUnprefixed status=test
FrameTimingSupport status=experimental FrameTimingSupport status=experimental
GamepadExtensions GamepadExtensions
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment