зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1697334 implement matches property in web_accessible_resources r=zombie,ckerschb,necko-reviewers,smaug
This patch implements support for the manifest V3 matches property which limits what hosts may load an extensions web_accessible_resources. Differential Revision: https://phabricator.services.mozilla.com/D107746
This commit is contained in:
Родитель
69ddccc340
Коммит
c4d682be93
|
@ -58,9 +58,10 @@ interface nsIAddonPolicyService : nsISupports
|
|||
AString getExtensionName(in AString aAddonId);
|
||||
|
||||
/**
|
||||
* Returns true if a given extension:// URI is web-accessible.
|
||||
* Returns true if a given extension:// URI is web-accessible and loadable by the source.
|
||||
* This should be called if the protocol flags for the extension URI has URI_WEB_ACCESSIBLE.
|
||||
*/
|
||||
boolean extensionURILoadableByAnyone(in nsIURI aURI);
|
||||
boolean sourceMayLoadExtensionURI(in nsIURI aSourceURI, in nsIURI aExtensionURI);
|
||||
|
||||
/**
|
||||
* Maps an extension URI to the ID of the addon it belongs to.
|
||||
|
|
|
@ -669,6 +669,21 @@ nsScriptSecurityManager::CheckLoadURIWithPrincipal(nsIPrincipal* aPrincipal,
|
|||
return NS_ERROR_DOM_BAD_URI;
|
||||
}
|
||||
|
||||
// Extensions may allow access to a web accessible resource.
|
||||
bool maybeWebAccessible = false;
|
||||
NS_URIChainHasFlags(targetBaseURI,
|
||||
nsIProtocolHandler::WEBEXT_URI_WEB_ACCESSIBLE,
|
||||
&maybeWebAccessible);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (maybeWebAccessible) {
|
||||
bool isWebAccessible = false;
|
||||
rv = ExtensionPolicyService::GetSingleton().SourceMayLoadExtensionURI(
|
||||
sourceURI, targetBaseURI, &isWebAccessible);
|
||||
if (!(NS_SUCCEEDED(rv) && isWebAccessible)) {
|
||||
return NS_ERROR_DOM_BAD_URI;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for uris that are only loadable by principals that subsume them
|
||||
bool targetURIIsLoadableBySubsumers = false;
|
||||
rv = NS_URIChainHasFlags(targetBaseURI,
|
||||
|
|
|
@ -161,9 +161,17 @@ interface WebExtensionPolicy {
|
|||
|
||||
/**
|
||||
* Returns true if the given path relative to the extension's moz-extension:
|
||||
* URL root may be accessed by web content.
|
||||
* URL root is listed as a web accessible path. Access checks on a path, such
|
||||
* as performed in nsScriptSecurityManager, use sourceMayAccessPath below.
|
||||
*/
|
||||
boolean isPathWebAccessible(DOMString pathname);
|
||||
boolean isWebAccessiblePath(DOMString pathname);
|
||||
|
||||
/**
|
||||
* Returns true if the given path relative to the extension's moz-extension:
|
||||
* URL root may be accessed by web content at sourceURI. For Manifest V2,
|
||||
* sourceURI is ignored and the path must merely be listed as web accessible.
|
||||
*/
|
||||
boolean sourceMayAccessPath(URI sourceURI, DOMString pathname);
|
||||
|
||||
/**
|
||||
* Replaces localization placeholders in the given string with localized
|
||||
|
@ -260,6 +268,7 @@ interface WebExtensionPolicy {
|
|||
|
||||
dictionary WebAccessibleResourceInit {
|
||||
required sequence<MatchGlobOrString> resources;
|
||||
MatchPatternSetOrStringSequence matches;
|
||||
};
|
||||
|
||||
dictionary WebExtensionInit {
|
||||
|
|
|
@ -309,4 +309,10 @@ interface nsIProtocolHandler : nsISupports
|
|||
* protocols. Only used in Mailnews (comm-central).
|
||||
*/
|
||||
const unsigned long URI_FORBIDS_COOKIE_ACCESS = (1 << 23);
|
||||
|
||||
/**
|
||||
* This is an extension web accessible uri that is loadable if checked
|
||||
* against an allow whitelist.
|
||||
*/
|
||||
const unsigned long WEBEXT_URI_WEB_ACCESSIBLE = (1 << 24);
|
||||
};
|
||||
|
|
|
@ -379,9 +379,13 @@ nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI,
|
|||
if (auto* policy = EPS().GetByURL(url)) {
|
||||
// In general a moz-extension URI is only loadable by chrome, but a
|
||||
// whitelisted subset are web-accessible (and cross-origin fetchable). Check
|
||||
// that whitelist.
|
||||
if (policy->IsPathWebAccessible(url.FilePath())) {
|
||||
flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE;
|
||||
// that whitelist. For Manifest V3 extensions, an additional whitelist
|
||||
// for the source loading the url must be checked so we add the flag
|
||||
// WEBEXT_URI_WEB_ACCESSIBLE, which is then checked in
|
||||
// nsScriptSecurityManager.
|
||||
if (policy->IsWebAccessiblePath(url.FilePath())) {
|
||||
flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE |
|
||||
WEBEXT_URI_WEB_ACCESSIBLE;
|
||||
} else {
|
||||
flags |= URI_DANGEROUS_TO_LOAD;
|
||||
}
|
||||
|
|
|
@ -575,11 +575,12 @@ nsresult ExtensionPolicyService::GetExtensionName(const nsAString& aAddonId,
|
|||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
nsresult ExtensionPolicyService::ExtensionURILoadableByAnyone(nsIURI* aURI,
|
||||
bool* aResult) {
|
||||
URLInfo url(aURI);
|
||||
nsresult ExtensionPolicyService::SourceMayLoadExtensionURI(
|
||||
nsIURI* aSourceURI, nsIURI* aExtensionURI, bool* aResult) {
|
||||
URLInfo source(aSourceURI);
|
||||
URLInfo url(aExtensionURI);
|
||||
if (WebExtensionPolicy* policy = GetByURL(url)) {
|
||||
*aResult = policy->IsPathWebAccessible(url.FilePath());
|
||||
*aResult = policy->SourceMayAccessPath(source, url.FilePath());
|
||||
return NS_OK;
|
||||
}
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
|
|
|
@ -139,6 +139,16 @@ WebAccessibleResource::WebAccessibleResource(
|
|||
GlobalObject& aGlobal, const WebAccessibleResourceInit& aInit,
|
||||
ErrorResult& aRv) {
|
||||
ParseGlobs(aGlobal, aInit.mResources, mWebAccessiblePaths, aRv);
|
||||
if (aRv.Failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aInit.mMatches.WasPassed()) {
|
||||
MatchPatternOptions options;
|
||||
options.mRestrictSchemes = true;
|
||||
mMatches = ParseMatches(aGlobal, aInit.mMatches.Value(), options,
|
||||
ErrorBehavior::CreateEmptyPattern, aRv);
|
||||
}
|
||||
}
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAccessibleResource)
|
||||
|
|
|
@ -44,15 +44,21 @@ class WebAccessibleResource final : public nsISupports {
|
|||
const WebAccessibleResourceInit& aInit,
|
||||
ErrorResult& aRv);
|
||||
|
||||
bool IsPathWebAccessible(const nsAString& aPath) const {
|
||||
bool IsWebAccessiblePath(const nsAString& aPath) const {
|
||||
return mWebAccessiblePaths.Matches(aPath);
|
||||
}
|
||||
|
||||
bool SourceMayAccessPath(const URLInfo& aURI, const nsAString& aPath) {
|
||||
return mWebAccessiblePaths.Matches(aPath) && mMatches &&
|
||||
mMatches->Matches(aURI);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual ~WebAccessibleResource() = default;
|
||||
|
||||
private:
|
||||
MatchGlobSet mWebAccessiblePaths;
|
||||
RefPtr<MatchPatternSet> mMatches;
|
||||
};
|
||||
|
||||
class WebExtensionPolicy final : public nsISupports,
|
||||
|
@ -98,9 +104,21 @@ class WebExtensionPolicy final : public nsISupports,
|
|||
bool aCheckRestricted = true,
|
||||
bool aAllowFilePermission = false) const;
|
||||
|
||||
bool IsPathWebAccessible(const nsAString& aPath) const {
|
||||
bool IsWebAccessiblePath(const nsAString& aPath) const {
|
||||
for (const auto& resource : mWebAccessibleResources) {
|
||||
if (resource->IsPathWebAccessible(aPath)) {
|
||||
if (resource->IsWebAccessiblePath(aPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SourceMayAccessPath(const URLInfo& aURI, const nsAString& aPath) const {
|
||||
if (mManifestVersion < 3) {
|
||||
return IsWebAccessiblePath(aPath);
|
||||
}
|
||||
for (const auto& resource : mWebAccessibleResources) {
|
||||
if (resource->SourceMayAccessPath(aURI, aPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,6 +249,10 @@
|
|||
"resources": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "MatchPatternRestricted" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,15 +120,15 @@ add_task(async function test_WebExtensionPolicy() {
|
|||
// Web-accessible resources
|
||||
|
||||
ok(
|
||||
policy.isPathWebAccessible("/foo/bar"),
|
||||
policy.isWebAccessiblePath("/foo/bar"),
|
||||
"Web-accessible glob should be web-accessible"
|
||||
);
|
||||
ok(
|
||||
policy.isPathWebAccessible("/bar.baz"),
|
||||
policy.isWebAccessiblePath("/bar.baz"),
|
||||
"Web-accessible path should be web-accessible"
|
||||
);
|
||||
ok(
|
||||
!policy.isPathWebAccessible("/bar.baz/quux"),
|
||||
!policy.isWebAccessiblePath("/bar.baz/quux"),
|
||||
"Non-web-accessible path should not be web-accessible"
|
||||
);
|
||||
|
||||
|
|
|
@ -137,7 +137,9 @@ async function testPolicy(manifest_version = 2, customCSP = null) {
|
|||
extension_pages,
|
||||
};
|
||||
let resources = web_accessible_resources;
|
||||
web_accessible_resources = [{ resources }];
|
||||
web_accessible_resources = [
|
||||
{ resources, matches: ["http://example.com/*"] },
|
||||
];
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
|
@ -178,7 +180,7 @@ async function testPolicy(manifest_version = 2, customCSP = null) {
|
|||
let frameScriptURL = `data:,(${encodeURI(frameScript)}).call(this)`;
|
||||
Services.mm.loadFrameScript(frameScriptURL, true, true);
|
||||
|
||||
info(`Testing CSP for policy: ${content_security_policy}`);
|
||||
info(`Testing CSP for policy: ${JSON.stringify(content_security_policy)}`);
|
||||
|
||||
await extension.startup();
|
||||
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
"use strict";
|
||||
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
|
||||
|
||||
const server = createHttpServer({ hosts: ["example.com", "example.org"] });
|
||||
server.registerDirectory("/data/", do_get_file("data"));
|
||||
|
||||
let image = atob(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
|
||||
"ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
|
||||
);
|
||||
const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0))
|
||||
.buffer;
|
||||
|
||||
add_task(async function test_web_accessible_resources() {
|
||||
async function contentScript() {
|
||||
let canLoad = window.location.href.startsWith("http://example.com");
|
||||
|
||||
let urls = [
|
||||
{
|
||||
name: "iframe",
|
||||
url: browser.runtime.getURL("accessible.html"),
|
||||
shouldLoad: canLoad,
|
||||
},
|
||||
{
|
||||
name: "iframe",
|
||||
url: browser.runtime.getURL("inaccessible.html"),
|
||||
shouldLoad: false,
|
||||
},
|
||||
{
|
||||
name: "img",
|
||||
url: browser.runtime.getURL("image.png"),
|
||||
shouldLoad: canLoad,
|
||||
},
|
||||
{
|
||||
name: "script",
|
||||
url: browser.runtime.getURL("script.js"),
|
||||
shouldLoad: canLoad,
|
||||
},
|
||||
];
|
||||
|
||||
function test_element_src(name, url) {
|
||||
return new Promise(resolve => {
|
||||
let elem = document.createElement(name);
|
||||
// Set the src via wrappedJSObject so the load is triggered with the
|
||||
// content page's principal rather than ours.
|
||||
elem.wrappedJSObject.setAttribute("src", url);
|
||||
elem.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
resolve(true);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
elem.addEventListener(
|
||||
"error",
|
||||
() => {
|
||||
resolve(false);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
document.body.appendChild(elem);
|
||||
});
|
||||
}
|
||||
for (let test of urls) {
|
||||
let loaded = await test_element_src(test.name, test.url);
|
||||
browser.test.assertEq(loaded, test.shouldLoad, "resource loaded");
|
||||
}
|
||||
browser.test.notifyPass("web-accessible-resources");
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ["http://example.com/data/*", "http://example.org/data/*"],
|
||||
js: ["content_script.js"],
|
||||
run_at: "document_idle",
|
||||
},
|
||||
],
|
||||
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ["/accessible.html", "/image.png", "/script.js"],
|
||||
matches: ["http://example.com/data/*"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
files: {
|
||||
"content_script.js": contentScript,
|
||||
|
||||
"accessible.html": `<html><head>
|
||||
<meta charset="utf-8">
|
||||
</head></html>`,
|
||||
|
||||
"inaccessible.html": `<html><head>
|
||||
<meta charset="utf-8">
|
||||
</head></html>`,
|
||||
|
||||
"image.png": IMAGE_ARRAYBUFFER,
|
||||
"script.js": () => {
|
||||
// empty script
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
|
||||
let page = await ExtensionTestUtils.loadContentPage(
|
||||
"http://example.com/data/"
|
||||
);
|
||||
|
||||
await extension.awaitFinish("web-accessible-resources");
|
||||
await page.close();
|
||||
|
||||
// None of the test resources are loadable in example.org
|
||||
page = await ExtensionTestUtils.loadContentPage("http://example.org/data/");
|
||||
|
||||
await extension.awaitFinish("web-accessible-resources");
|
||||
|
||||
await page.close();
|
||||
await extension.unload();
|
||||
});
|
|
@ -16,3 +16,4 @@ skip-if = !nightly_build
|
|||
[test_ext_shadowdom.js]
|
||||
skip-if = ccov && os == 'linux' # bug 1607581
|
||||
[test_ext_web_accessible_resources.js]
|
||||
[test_ext_web_accessible_resources_matches.js]
|
||||
|
|
Загрузка…
Ссылка в новой задаче