Commit 3e61366f authored by Yutaka Hirano's avatar Yutaka Hirano Committed by Commit Bot

[Extensions] Have web accessible resources loadable from COEP frames

Currently web accessible resources in Chrome extensions[A] cannot be
loaded as an iframe via content scripts from two reasons.

1. The response does not have a cross-origin-embedder-policy header.
2. The response does not have a cross-origin-resource-policy header.

To fix the issue, we make iframes inherit parent COEP implicitly for
chrome-extension:// URLs. This is similar to blob:// URLs. We also
attach cross-origin-resource-policy: cross-origin to every web
accessible resource. This is aligned with the fact that it also has
access-control-allow-origin: *.


A: https://developer.chrome.com/extensions/manifest/web_accessible_resources

Bug: 1085915
Change-Id: I4574b4dbf55ab2d815e9b6ab22fd7b0a8ab28887
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2212200
Commit-Queue: Yutaka Hirano <yhirano@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarMike West <mkwst@chromium.org>
Cr-Commit-Position: refs/heads/master@{#773957}
parent a6881107
...@@ -2256,7 +2256,6 @@ void ChromeContentBrowserClient::AppendExtraCommandLineSwitches( ...@@ -2256,7 +2256,6 @@ void ChromeContentBrowserClient::AppendExtraCommandLineSwitches(
command_line->AppendSwitch(switches::kAllowSyncXHRInPageDismissal); command_line->AppendSwitch(switches::kAllowSyncXHRInPageDismissal);
} }
if (prefs->HasPrefPath(prefs::kScrollToTextFragmentEnabled) && if (prefs->HasPrefPath(prefs::kScrollToTextFragmentEnabled) &&
!prefs->GetBoolean(prefs::kScrollToTextFragmentEnabled)) { !prefs->GetBoolean(prefs::kScrollToTextFragmentEnabled)) {
command_line->AppendSwitch(switches::kDisableScrollToTextFragment); command_line->AppendSwitch(switches::kDisableScrollToTextFragment);
...@@ -5707,3 +5706,12 @@ bool ChromeContentBrowserClient::IsOriginTrialRequiredForAppCache( ...@@ -5707,3 +5706,12 @@ bool ChromeContentBrowserClient::IsOriginTrialRequiredForAppCache(
return false; return false;
} }
bool ChromeContentBrowserClient::
ShouldInheritCrossOriginEmbedderPolicyImplicitly(const GURL& url) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
return url.SchemeIs(extensions::kExtensionScheme);
#else
return false;
#endif
}
...@@ -676,6 +676,8 @@ class ChromeContentBrowserClient : public content::ContentBrowserClient { ...@@ -676,6 +676,8 @@ class ChromeContentBrowserClient : public content::ContentBrowserClient {
bool IsOriginTrialRequiredForAppCache( bool IsOriginTrialRequiredForAppCache(
content::BrowserContext* browser_context) override; content::BrowserContext* browser_context) override;
bool ShouldInheritCrossOriginEmbedderPolicyImplicitly(
const GURL& url) override;
protected: protected:
static bool HandleWebUI(GURL* url, content::BrowserContext* browser_context); static bool HandleWebUI(GURL* url, content::BrowserContext* browser_context);
......
...@@ -46,6 +46,8 @@ ...@@ -46,6 +46,8 @@
#include "extensions/test/test_extension_dir.h" #include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h" #include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "ui/base/page_transition_types.h" #include "ui/base/page_transition_types.h"
#include "url/gurl.h" #include "url/gurl.h"
...@@ -72,7 +74,7 @@ testing::AssertionResult CheckStyleInjection(Browser* browser, ...@@ -72,7 +74,7 @@ testing::AssertionResult CheckStyleInjection(Browser* browser,
" getPropertyValue('display') == 'none');", " getPropertyValue('display') == 'none');",
&css_injected)) { &css_injected)) {
return testing::AssertionFailure() return testing::AssertionFailure()
<< "Failed to execute script and extract bool for injection status."; << "Failed to execute script and extract bool for injection status.";
} }
if (css_injected != expected_injection) { if (css_injected != expected_injection) {
...@@ -91,11 +93,12 @@ testing::AssertionResult CheckStyleInjection(Browser* browser, ...@@ -91,11 +93,12 @@ testing::AssertionResult CheckStyleInjection(Browser* browser,
" document.styleSheets.length == 0);", " document.styleSheets.length == 0);",
&css_doesnt_add_to_list)) { &css_doesnt_add_to_list)) {
return testing::AssertionFailure() return testing::AssertionFailure()
<< "Failed to execute script and extract bool for stylesheets length."; << "Failed to execute script and extract bool for stylesheets "
"length.";
} }
if (!css_doesnt_add_to_list) { if (!css_doesnt_add_to_list) {
return testing::AssertionFailure() return testing::AssertionFailure()
<< "CSS injection added to number of stylesheets."; << "CSS injection added to number of stylesheets.";
} }
return testing::AssertionSuccess(); return testing::AssertionSuccess();
...@@ -190,8 +193,8 @@ IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptExtensionIframe) { ...@@ -190,8 +193,8 @@ IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptExtensionIframe) {
IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptExtensionProcess) { IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptExtensionProcess) {
ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(StartEmbeddedTestServer());
ASSERT_TRUE( ASSERT_TRUE(RunExtensionTest("content_scripts/extension_process"))
RunExtensionTest("content_scripts/extension_process")) << message_; << message_;
} }
IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptFragmentNavigation) { IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptFragmentNavigation) {
...@@ -214,8 +217,8 @@ IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptIsolatedWorlds) { ...@@ -214,8 +217,8 @@ IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, ContentScriptIsolatedWorlds) {
IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, IN_PROC_BROWSER_TEST_F(ContentScriptApiTest,
ContentScriptIgnoreHostPermissions) { ContentScriptIgnoreHostPermissions) {
ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(StartEmbeddedTestServer());
ASSERT_TRUE(RunExtensionTest( ASSERT_TRUE(RunExtensionTest("content_scripts/dont_match_host_permissions"))
"content_scripts/dont_match_host_permissions")) << message_; << message_;
} }
// crbug.com/39249 -- content scripts js should not run on view source. // crbug.com/39249 -- content scripts js should not run on view source.
...@@ -353,7 +356,7 @@ IN_PROC_BROWSER_TEST_F(ContentScriptCssInjectionTest, ...@@ -353,7 +356,7 @@ IN_PROC_BROWSER_TEST_F(ContentScriptCssInjectionTest,
ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(StartEmbeddedTestServer());
ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("content_scripts") ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("content_scripts")
.AppendASCII("css_injection"))); .AppendASCII("css_injection")));
// CSS injection should be allowed on an aribitrary web page. // CSS injection should be allowed on an aribitrary web page.
GURL url = GURL url =
...@@ -368,7 +371,8 @@ IN_PROC_BROWSER_TEST_F(ContentScriptCssInjectionTest, ...@@ -368,7 +371,8 @@ IN_PROC_BROWSER_TEST_F(ContentScriptCssInjectionTest,
// We disallow all injection on the webstore. // We disallow all injection on the webstore.
GURL::Replacements replacements; GURL::Replacements replacements;
replacements.SetHostStr(kWebstoreDomain); replacements.SetHostStr(kWebstoreDomain);
url = embedded_test_server()->GetURL("/extensions/test_file_with_body.html") url = embedded_test_server()
->GetURL("/extensions/test_file_with_body.html")
.ReplaceComponents(replacements); .ReplaceComponents(replacements);
EXPECT_TRUE(CheckStyleInjection(browser(), url, false)); EXPECT_TRUE(CheckStyleInjection(browser(), url, false));
} }
...@@ -1330,4 +1334,38 @@ IN_PROC_BROWSER_TEST_F(NTPInterceptionTest, ContentScript) { ...@@ -1330,4 +1334,38 @@ IN_PROC_BROWSER_TEST_F(NTPInterceptionTest, ContentScript) {
EXPECT_FALSE(script_injected_in_ntp); EXPECT_FALSE(script_injected_in_ntp);
} }
IN_PROC_BROWSER_TEST_F(ContentScriptApiTest, CoepFrameTest) {
using HttpRequest = net::test_server::HttpRequest;
using HttpResponse = net::test_server::HttpResponse;
// We have a separate server because COEP only works in secure contexts.
net::EmbeddedTestServer server(net::EmbeddedTestServer::TYPE_HTTPS);
server.RegisterRequestHandler(base::BindRepeating(
[](const HttpRequest& request) -> std::unique_ptr<HttpResponse> {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content_type("text/html");
response->AddCustomHeader("cross-origin-embedder-policy",
"require-corp");
response->set_content("<!doctpye html><html></html>");
return response;
}));
const extensions::Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("content_scripts/coep_frame"));
ASSERT_TRUE(extension);
auto handle = server.StartAndReturnHandle();
const GURL url = server.GetURL("/hello.html");
ui_test_utils::NavigateToURL(browser(), url);
const auto kPassed = base::ASCIIToUTF16("PASSED");
const auto kFailed = base::ASCIIToUTF16("FAILED");
content::TitleWatcher watcher(
browser()->tab_strip_model()->GetActiveWebContents(), kPassed);
watcher.AlsoWaitForTitle(kFailed);
ASSERT_EQ(kPassed, watcher.WaitAndGetTitle());
}
} // namespace extensions } // namespace extensions
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async () => {
try {
const iframe = document.createElement('iframe');
iframe.src = chrome.runtime.getURL('iframe.html?href=' + location.href);
document.body.appendChild(iframe);
await Promise.all([
new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = reject;
}),
new Promise((resolve, reject) => {
// The fetch request made from |iframe| is a no-cors cross-origin
// request, and the response doesn't have a CORP header.
// Hence it should be blocked (note that COEP is enabled on *this*
// frame, and hence on |iframe|).
self.addEventListener('message', (e) => {
if (e.data === 'SUCCESS') {
reject(Error('fetch succeeded unexpectedly'));
return;
}
if (e.data === 'FAIL') {
resolve();
}
});
})
]);
document.title = 'PASSED';
} catch (e) {
document.title = 'FAILED';
}
})();
<!doctype html>
<html>
<script src="script.js"></script>
</html>
\ No newline at end of file
{
"name": "Load an iframe from content script",
"version": "0.1",
"manifest_version": 2,
"description": "Web accessible resource can be loaded as an iframe in a COEP frame.",
"permissions": ["https://*/*"],
"content_scripts": [
{
"matches": ["https://*/hello.html"],
"js": ["content-script.js"]
}
],
"web_accessible_resources": [
"iframe.html",
"script.js"
]
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const params = new URLSearchParams(location.search);
fetch(params.get('href'), {mode: 'no-cors'}).then(() => {
parent.postMessage('SUCCESS', '*');
}, () => {
parent.postMessage('FAIL', '*');
});
\ No newline at end of file
...@@ -2003,7 +2003,10 @@ void NavigationRequest::OnResponseStarted( ...@@ -2003,7 +2003,10 @@ void NavigationRequest::OnResponseStarted(
// Some special URLs not loaded using the network are inheriting the // Some special URLs not loaded using the network are inheriting the
// Cross-Origin-Embedder-Policy header from their parent. // Cross-Origin-Embedder-Policy header from their parent.
const bool has_allowed_scheme = const bool has_allowed_scheme =
url.SchemeIsBlob() || url.SchemeIs(url::kDataScheme); url.SchemeIsBlob() || url.SchemeIs(url::kDataScheme) ||
GetContentClient()
->browser()
->ShouldInheritCrossOriginEmbedderPolicyImplicitly(url);
if (parent_coep.value == kRequireCorp && has_allowed_scheme) { if (parent_coep.value == kRequireCorp && has_allowed_scheme) {
cross_origin_embedder_policy.value = kRequireCorp; cross_origin_embedder_policy.value = kRequireCorp;
} }
......
...@@ -1098,4 +1098,9 @@ bool ContentBrowserClient::IsOriginTrialRequiredForAppCache( ...@@ -1098,4 +1098,9 @@ bool ContentBrowserClient::IsOriginTrialRequiredForAppCache(
void ContentBrowserClient::BindBrowserControlInterface( void ContentBrowserClient::BindBrowserControlInterface(
mojo::GenericPendingReceiver receiver) {} mojo::GenericPendingReceiver receiver) {}
bool ContentBrowserClient::ShouldInheritCrossOriginEmbedderPolicyImplicitly(
const GURL& url) {
return false;
}
} // namespace content } // namespace content
...@@ -1848,6 +1848,11 @@ class CONTENT_EXPORT ContentBrowserClient { ...@@ -1848,6 +1848,11 @@ class CONTENT_EXPORT ContentBrowserClient {
// request received from an external client is passed to this method. // request received from an external client is passed to this method.
virtual void BindBrowserControlInterface( virtual void BindBrowserControlInterface(
mojo::GenericPendingReceiver receiver); mojo::GenericPendingReceiver receiver);
// Returns true when a context (e.g., iframe) whose URL is |url| should
// inherit the parent COEP value implicitly, similar to "blob:"
virtual bool ShouldInheritCrossOriginEmbedderPolicyImplicitly(
const GURL& url);
}; };
} // namespace content } // namespace content
......
...@@ -641,6 +641,8 @@ scoped_refptr<net::HttpResponseHeaders> BuildHttpHeaders( ...@@ -641,6 +641,8 @@ scoped_refptr<net::HttpResponseHeaders> BuildHttpHeaders(
if (send_cors_header) { if (send_cors_header) {
raw_headers.append(1, '\0'); raw_headers.append(1, '\0');
raw_headers.append("Access-Control-Allow-Origin: *"); raw_headers.append("Access-Control-Allow-Origin: *");
raw_headers.append(1, '\0');
raw_headers.append("Cross-Origin-Resource-Policy: cross-origin");
} }
if (!last_modified_time.is_null()) { if (!last_modified_time.is_null()) {
......
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