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:
Shane Caraveo 2021-05-14 03:15:15 +00:00
Родитель 69ddccc340
Коммит c4d682be93
13 изменённых файлов: 214 добавлений и 19 удалений

Просмотреть файл

@ -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]