Commit befd0655 authored by mkwst@chromium.org's avatar mkwst@chromium.org

CSP: 'frame-ancestors' should override 'x-frame-options'.

As specified in [1], the 'frame-ancestors' CSP directive should take
control of the access checks when loading a document. In particular,
the 'x-frame-options' header should be ignored if a 'frame-ancestors'
directive is present and enforced.

[1]: https://w3c.github.io/webappsec/specs/content-security-policy/#frame-ancestors-and-frame-options

BUG=510423
R=estark@chromium.org

Review URL: https://codereview.chromium.org/1326823003

git-svn-id: svn://svn.chromium.org/blink/trunk@201959 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent 6986f59c
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<script>
async_test(function (t) {
var i = document.createElement('iframe');
i.src = "../../resources/frame-ancestors-and-x-frame-options.pl?policy='self'&xfo=DENY";
i.onload = t.step_func_done(function () {
assert_equals(i.contentDocument.origin, document.origin, "The same-origin page loaded.");
});
document.body.appendChild(i);
}, "A 'frame-ancestors' CSP directive overrides an 'x-frame-options' header which would block the page.");
async_test(function (t) {
var i = document.createElement('iframe');
i.src = "../../resources/frame-ancestors-and-x-frame-options.pl?policy=other-origin.com&xfo=SAMEORIGIN";
i.onload = t.step_func_done(function () {
assert_throws(
"SecurityError",
function () { i.contentDocument.origin },
"The same-origin page was blocked and sandboxed.");
});
document.body.appendChild(i);
}, "A 'frame-ancestors' CSP directive overrides an 'x-frame-options' header which would allow the page.");
</script>
</body>
</html>
#!/usr/bin/perl
use strict;
use CGI;
my $cgi = new CGI;
print "Content-Type: text/html; charset=UTF-8\n";
print "Content-Security-Policy: frame-ancestors " . $cgi->param("policy") . "\n";
print "X-Frame-Options: " . $cgi->param("xfo") . "\n\n";
print "<!DOCTYPE html>\n";
print "<html>\n";
print "<body>\n";
print " <p>This is an IFrame sending a Content Security Policy header containing \"frame-ancestors " . $cgi->param("policy") . "\" and \"X-Frame-Options: " . $cgi->param("xfo") . "\".</p>\n";
print "</body>\n";
print "</html>\n";
......@@ -70,6 +70,7 @@ public:
bool didSetReferrerPolicy() const { return m_didSetReferrerPolicy; }
bool isReportOnly() const { return m_reportOnly; }
const Vector<String>& reportEndpoints() const { return m_reportEndpoints; }
bool isFrameAncestorsEnforced() const { return m_frameAncestors.get() && !m_reportOnly; }
// Used to copy plugin-types into a plugin document in a nested
// browsing context.
......
......@@ -615,6 +615,15 @@ bool ContentSecurityPolicy::allowAncestors(LocalFrame* frame, const KURL& url, C
return isAllowedByAllWithFrame<&CSPDirectiveList::allowAncestors>(m_policies, frame, url, reportingStatus);
}
bool ContentSecurityPolicy::isFrameAncestorsEnforced() const
{
for (const auto& policy : m_policies) {
if (policy->isFrameAncestorsEnforced())
return true;
}
return false;
}
bool ContentSecurityPolicy::isActive() const
{
return !m_policies.isEmpty();
......
......@@ -176,6 +176,7 @@ public:
// because a child frame can't manipulate the URL of a cross-origin
// parent.
bool allowAncestors(LocalFrame*, const KURL&, ReportingStatus = SendReport) const;
bool isFrameAncestorsEnforced() const;
// The nonce and hash allow functions are guaranteed to not have any side
// effects, including reporting.
......
......@@ -92,4 +92,16 @@ TEST_F(ContentSecurityPolicyTest, CopyPluginTypesFrom)
EXPECT_FALSE(csp2->allowPluginType("application/x-type-2", "application/x-type-2", exampleUrl, ContentSecurityPolicy::SuppressReport));
}
TEST_F(ContentSecurityPolicyTest, IsFrameAncestorsEnforced)
{
csp->didReceiveHeader("script-src 'none';", ContentSecurityPolicyHeaderTypeEnforce, ContentSecurityPolicyHeaderSourceHTTP);
EXPECT_FALSE(csp->isFrameAncestorsEnforced());
csp->didReceiveHeader("frame-ancestors 'self'", ContentSecurityPolicyHeaderTypeReport, ContentSecurityPolicyHeaderSourceHTTP);
EXPECT_FALSE(csp->isFrameAncestorsEnforced());
csp->didReceiveHeader("frame-ancestors 'self'", ContentSecurityPolicyHeaderTypeEnforce, ContentSecurityPolicyHeaderSourceHTTP);
EXPECT_TRUE(csp->isFrameAncestorsEnforced());
}
} // namespace
......@@ -469,21 +469,6 @@ void DocumentLoader::responseReceived(Resource* resource, const ResourceResponse
if (response.appCacheID())
memoryCache()->remove(m_mainResource.get());
DEFINE_STATIC_LOCAL(AtomicString, xFrameOptionHeader, ("x-frame-options", AtomicString::ConstructFromLiteral));
HTTPHeaderMap::const_iterator it = response.httpHeaderFields().find(xFrameOptionHeader);
if (it != response.httpHeaderFields().end()) {
String content = it->value;
if (frameLoader()->shouldInterruptLoadForXFrameOptions(content, response.url(), mainResourceIdentifier())) {
String message = "Refused to display '" + response.url().elidedString() + "' in a frame because it set 'X-Frame-Options' to '" + content + "'.";
RefPtrWillBeRawPtr<ConsoleMessage> consoleMessage = ConsoleMessage::create(SecurityMessageSource, ErrorMessageLevel, message);
consoleMessage->setRequestIdentifier(mainResourceIdentifier());
frame()->document()->addConsoleMessage(consoleMessage.release());
cancelLoadAfterXFrameOptionsOrCSPDenied(response);
return;
}
}
m_contentSecurityPolicy = ContentSecurityPolicy::create();
m_contentSecurityPolicy->setOverrideURLForSelf(response.url());
m_contentSecurityPolicy->didReceiveHeaders(ContentSecurityPolicyResponseHeaders(response));
......@@ -492,6 +477,25 @@ void DocumentLoader::responseReceived(Resource* resource, const ResourceResponse
return;
}
DEFINE_STATIC_LOCAL(AtomicString, xFrameOptionHeader, ("x-frame-options", AtomicString::ConstructFromLiteral));
// 'frame-ancestors' obviates 'x-frame-options': https://w3c.github.io/webappsec/specs/content-security-policy/#frame-ancestors-and-frame-options
if (!m_contentSecurityPolicy->isFrameAncestorsEnforced()) {
HTTPHeaderMap::const_iterator it = response.httpHeaderFields().find(xFrameOptionHeader);
if (it != response.httpHeaderFields().end()) {
String content = it->value;
if (frameLoader()->shouldInterruptLoadForXFrameOptions(content, response.url(), mainResourceIdentifier())) {
String message = "Refused to display '" + response.url().elidedString() + "' in a frame because it set 'X-Frame-Options' to '" + content + "'.";
RefPtrWillBeRawPtr<ConsoleMessage> consoleMessage = ConsoleMessage::create(SecurityMessageSource, ErrorMessageLevel, message);
consoleMessage->setRequestIdentifier(mainResourceIdentifier());
frame()->document()->addConsoleMessage(consoleMessage.release());
cancelLoadAfterXFrameOptionsOrCSPDenied(response);
return;
}
}
}
ASSERT(!mainResourceLoader() || !mainResourceLoader()->defersLoading());
m_response = response;
......
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