Bug 1711168 support extension matching in webAccessibleResources r=zombie,smaug,rpl

Differential Revision: https://phabricator.services.mozilla.com/D115114
This commit is contained in:
Shane Caraveo 2022-07-26 19:39:14 +00:00
Родитель 180d0769a9
Коммит a73cd6df49
11 изменённых файлов: 370 добавлений и 42 удалений

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

@ -18,6 +18,7 @@
#include "nsAboutProtocolUtils.h"
#include "ThirdPartyUtil.h"
#include "mozilla/ContentPrincipal.h"
#include "mozilla/ExtensionPolicyService.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/dom/BlobURLProtocolHandler.h"
#include "mozilla/dom/ChromeUtils.h"
@ -596,6 +597,8 @@ nsresult BasePrincipal::CheckMayLoadHelper(nsIURI* aURI,
}
}
// Web Accessible Resources in MV2 Extensions are marked with
// URI_FETCHABLE_BY_ANYONE
bool fetchableByAnyone;
rv = NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_FETCHABLE_BY_ANYONE,
&fetchableByAnyone);
@ -603,16 +606,34 @@ nsresult BasePrincipal::CheckMayLoadHelper(nsIURI* aURI,
return NS_OK;
}
if (aReport) {
nsCOMPtr<nsIURI> prinURI;
rv = GetURI(getter_AddRefs(prinURI));
if (NS_SUCCEEDED(rv) && prinURI) {
nsScriptSecurityManager::ReportError(
"CheckSameOriginError", prinURI, aURI,
mOriginAttributes.mPrivateBrowsingId > 0, aInnerWindowID);
// Get the principal uri for the last flag check or error.
nsCOMPtr<nsIURI> prinURI;
rv = GetURI(getter_AddRefs(prinURI));
if (!(NS_SUCCEEDED(rv) && prinURI)) {
return NS_ERROR_DOM_BAD_URI;
}
// If MV3 Extension uris are web accessible by this principal it is allowed to
// load.
bool maybeWebAccessible = false;
NS_URIChainHasFlags(aURI, nsIProtocolHandler::WEBEXT_URI_WEB_ACCESSIBLE,
&maybeWebAccessible);
NS_ENSURE_SUCCESS(rv, rv);
if (maybeWebAccessible) {
bool isWebAccessible = false;
rv = ExtensionPolicyService::GetSingleton().SourceMayLoadExtensionURI(
prinURI, aURI, &isWebAccessible);
if (NS_SUCCEEDED(rv) && isWebAccessible) {
return NS_OK;
}
}
if (aReport) {
nsScriptSecurityManager::ReportError(
"CheckSameOriginError", prinURI, aURI,
mOriginAttributes.mPrivateBrowsingId > 0, aInnerWindowID);
}
return NS_ERROR_DOM_BAD_URI;
}

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

@ -741,21 +741,6 @@ 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,
@ -829,6 +814,7 @@ nsScriptSecurityManager::CheckLoadURIWithPrincipal(nsIPrincipal* aPrincipal,
bool schemesMatch =
scheme.Equals(otherScheme, nsCaseInsensitiveCStringComparator);
bool isSamePage = false;
bool isExtensionMismatch = false;
// about: URIs are special snowflakes.
if (scheme.EqualsLiteral("about") && schemesMatch) {
nsAutoCString moduleName, otherModuleName;
@ -876,6 +862,13 @@ nsScriptSecurityManager::CheckLoadURIWithPrincipal(nsIPrincipal* aPrincipal,
}
}
}
} else if (schemesMatch && scheme.EqualsLiteral("moz-extension")) {
// If it is not the same exension, we want to ensure we end up
// calling CheckLoadURIFlags
nsAutoCString host, otherHost;
currentURI->GetHost(host);
currentOtherURI->GetHost(otherHost);
isExtensionMismatch = !host.Equals(otherHost);
} else {
bool equalExceptRef = false;
rv = currentURI->EqualsExceptRef(currentOtherURI, &equalExceptRef);
@ -884,10 +877,12 @@ nsScriptSecurityManager::CheckLoadURIWithPrincipal(nsIPrincipal* aPrincipal,
// If schemes are not equal, or they're equal but the target URI
// is different from the source URI and doesn't always allow linking
// from the same scheme, check if the URI flags of the current target
// URI allow the current source URI to link to it.
// from the same scheme, or this is two different extensions, check
// if the URI flags of the current target URI allow the current
// source URI to link to it.
// The policy is specified by the protocol flags on both URIs.
if (!schemesMatch || (denySameSchemeLinks && !isSamePage)) {
if (!schemesMatch || (denySameSchemeLinks && !isSamePage) ||
isExtensionMismatch) {
return CheckLoadURIFlags(
currentURI, currentOtherURI, sourceBaseURI, targetBaseURI, aFlags,
aPrincipal->OriginAttributesRef().mPrivateBrowsingId > 0,
@ -936,7 +931,8 @@ nsresult nsScriptSecurityManager::CheckLoadURIFlags(
nsresult rv = aTargetBaseURI->GetScheme(targetScheme);
if (NS_FAILED(rv)) return rv;
// Check for system target URI
// Check for system target URI. Regular (non web accessible) extension
// URIs will also have URI_DANGEROUS_TO_LOAD.
rv = DenyAccessIfURIHasFlags(aTargetURI,
nsIProtocolHandler::URI_DANGEROUS_TO_LOAD);
if (NS_FAILED(rv)) {
@ -962,6 +958,26 @@ nsresult nsScriptSecurityManager::CheckLoadURIFlags(
}
}
// If MV3 Extension uris are web accessible they have
// WEBEXT_URI_WEB_ACCESSIBLE.
bool maybeWebAccessible = false;
NS_URIChainHasFlags(aTargetURI, nsIProtocolHandler::WEBEXT_URI_WEB_ACCESSIBLE,
&maybeWebAccessible);
NS_ENSURE_SUCCESS(rv, rv);
if (maybeWebAccessible) {
bool isWebAccessible = false;
rv = ExtensionPolicyService::GetSingleton().SourceMayLoadExtensionURI(
aSourceURI, aTargetURI, &isWebAccessible);
if (NS_SUCCEEDED(rv) && isWebAccessible) {
return NS_OK;
}
if (reportErrors) {
ReportError(errorTag, aSourceURI, aTargetURI, aFromPrivateWindow,
aInnerWindowID);
}
return NS_ERROR_DOM_BAD_URI;
}
// Check for chrome target URI
bool targetURIIsUIResource = false;
rv = NS_URIChainHasFlags(aTargetURI, nsIProtocolHandler::URI_IS_UI_RESOURCE,

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

@ -274,7 +274,8 @@ interface WebExtensionPolicy {
dictionary WebAccessibleResourceInit {
required sequence<MatchGlobOrString> resources;
MatchPatternSetOrStringSequence matches;
MatchPatternSetOrStringSequence? matches = null;
sequence<DOMString>? extension_ids = null;
};
dictionary WebExtensionInit {

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

@ -1248,7 +1248,7 @@ nsresult nsContentSecurityManager::CheckAllowLoadInPrivilegedAboutContext(
}
/*
* Every protocol handler must set one of the five security flags
* Every protocol handler must set one of the six security flags
* defined in nsIProtocolHandler - if not - deny the load.
*/
nsresult nsContentSecurityManager::CheckChannelHasProtocolSecurityFlag(
@ -1273,6 +1273,9 @@ nsresult nsContentSecurityManager::CheckChannelHasProtocolSecurityFlag(
NS_ENSURE_SUCCESS(rv, rv);
uint32_t securityFlagsSet = 0;
if (flags & nsIProtocolHandler::WEBEXT_URI_WEB_ACCESSIBLE) {
securityFlagsSet += 1;
}
if (flags & nsIProtocolHandler::URI_LOADABLE_BY_ANYONE) {
securityFlagsSet += 1;
}

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

@ -275,7 +275,8 @@ interface nsIProtocolHandler : nsISupports
/**
* This URI may be fetched and the contents are visible to anyone. This is
* semantically equivalent to the resource being served with all-access CORS
* headers.
* headers. This is only used in MV2 Extensions and should not otherwise
* be used.
*/
const unsigned long URI_FETCHABLE_BY_ANYONE = (1 << 18);
@ -312,7 +313,7 @@ interface nsIProtocolHandler : nsISupports
/**
* This is an extension web accessible uri that is loadable if checked
* against an allow whitelist.
* against an allowlist using ExtensionPolicyService::SourceMayLoadExtensionURI.
*/
const unsigned long WEBEXT_URI_WEB_ACCESSIBLE = (1 << 24);
};

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

@ -416,15 +416,16 @@ nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI,
URLInfo url(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. 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.
// In general a moz-extension URI is only loadable by chrome, but an
// allowlist subset are web-accessible (and cross-origin fetchable).
// The allowlist is checked using EPS.SourceMayLoadExtensionURI in
// BasePrincipal and nsScriptSecurityManager.
if (policy->IsWebAccessiblePath(url.FilePath())) {
flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE |
WEBEXT_URI_WEB_ACCESSIBLE;
if (policy->ManifestVersion() < 3) {
flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE;
} else {
flags |= WEBEXT_URI_WEB_ACCESSIBLE;
}
} else {
flags |= URI_DANGEROUS_TO_LOAD;
}

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

@ -304,6 +304,18 @@ const POSTPROCESSORS = {
context.logError(context.makeError(msg));
throw new Error(msg);
},
webAccessibleMatching(value, context) {
// Ensure each object has at least one of matches or extension_ids array.
for (let obj of value) {
if (!obj.matches && !obj.extension_ids) {
const msg = `web_accessible_resources requires one of "matches" or "extension_ids"`;
context.logError(context.makeError(msg));
throw new Error(msg);
}
}
return value;
},
};
// Parses a regular expression, with support for the Python extended

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

@ -143,12 +143,24 @@ WebAccessibleResource::WebAccessibleResource(
return;
}
if (aInit.mMatches.WasPassed()) {
if (!aInit.mMatches.IsNull()) {
MatchPatternOptions options;
options.mRestrictSchemes = true;
mMatches = ParseMatches(aGlobal, aInit.mMatches.Value(), options,
ErrorBehavior::CreateEmptyPattern, aRv);
}
if (!aInit.mExtension_ids.IsNull()) {
mExtensionIDs = new AtomSet(aInit.mExtension_ids.Value());
}
}
bool WebAccessibleResource::IsExtensionMatch(const URLInfo& aURI) {
if (!mExtensionIDs) {
return false;
}
WebExtensionPolicy* policy = EPS().GetByHost(aURI.Host());
return policy && mExtensionIDs->Contains(policy->Id());
}
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAccessibleResource)

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

@ -50,16 +50,23 @@ class WebAccessibleResource final : public nsISupports {
}
bool SourceMayAccessPath(const URLInfo& aURI, const nsAString& aPath) {
return mWebAccessiblePaths.Matches(aPath) && mMatches &&
mMatches->Matches(aURI);
return mWebAccessiblePaths.Matches(aPath) &&
(IsHostMatch(aURI) || IsExtensionMatch(aURI));
}
bool IsHostMatch(const URLInfo& aURI) {
return mMatches && mMatches->Matches(aURI);
}
bool IsExtensionMatch(const URLInfo& aURI);
protected:
virtual ~WebAccessibleResource() = default;
private:
MatchGlobSet mWebAccessiblePaths;
RefPtr<MatchPatternSet> mMatches;
RefPtr<AtomSet> mExtensionIDs;
};
class WebExtensionPolicy final : public nsISupports,

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

@ -288,16 +288,27 @@
{
"min_manifest_version": 3,
"type": "array",
"postprocess": "webAccessibleMatching",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"resources": {
"type": "array",
"minItems": 1,
"items": { "type": "string" }
},
"matches": {
"optional": true,
"type": "array",
"minItems": 1,
"items": { "$ref": "MatchPattern" }
},
"extension_ids": {
"optional": true,
"type": "array",
"minItems": 1,
"items": { "$ref": "ExtensionID" }
}
}
}

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

@ -1,4 +1,5 @@
"use strict";
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
const server = createHttpServer({ hosts: ["example.com", "example.org"] });
@ -11,10 +12,77 @@ let image = atob(
const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0))
.buffer;
add_task(async function test_web_accessible_resources_matching() {
let extension = await ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
web_accessible_resources: [
{
resources: ["/accessible.html"],
},
],
},
});
await Assert.rejects(
extension.startup(),
/web_accessible_resources requires one of "matches" or "extension_ids"/,
"web_accessible_resources object format incorrect"
);
extension = await ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
web_accessible_resources: [
{
resources: ["/accessible.html"],
matches: ["http://example.com/data/*"],
},
],
},
});
await extension.startup();
ok(true, "web_accessible_resources with matches loads");
await extension.unload();
extension = await ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
web_accessible_resources: [
{
resources: ["/accessible.html"],
extension_ids: ["foo@mochitest"],
},
],
},
});
await extension.startup();
ok(true, "web_accessible_resources with extensions loads");
await extension.unload();
extension = await ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
web_accessible_resources: [
{
resources: ["/accessible.html"],
matches: ["http://example.com/data/*"],
extension_ids: ["foo@mochitest"],
},
],
},
});
await extension.startup();
ok(true, "web_accessible_resources with matches and extensions loads");
await extension.unload();
});
add_task(async function test_web_accessible_resources() {
async function contentScript() {
let canLoad = window.location.href.startsWith("http://example.com");
let urls = [
{
name: "iframe",
@ -136,3 +204,178 @@ add_task(async function test_web_accessible_resources() {
await page.close();
await extension.unload();
});
async function pageScript() {
function test_element_src(data) {
return new Promise(resolve => {
let elem = document.createElement(data.elem);
let elemContext =
data.content_context && elem.wrappedJSObject
? elem.wrappedJSObject
: elem;
elemContext.setAttribute("src", data.url);
elem.addEventListener(
"load",
() => {
browser.test.log(`got load event for ${data.url}`);
resolve(true);
},
{ once: true }
);
elem.addEventListener(
"error",
() => {
browser.test.log(`got error event for ${data.url}`);
resolve(false);
},
{ once: true }
);
document.body.appendChild(elem);
});
}
browser.test.onMessage.addListener(async msg => {
browser.test.log(`testing ${JSON.stringify(msg)}`);
let loaded = await test_element_src(msg);
browser.test.assertEq(loaded, msg.shouldLoad, `${msg.name} loaded`);
browser.test.sendMessage("web-accessible-resources");
});
browser.test.sendMessage("page-loaded");
}
add_task(async function test_web_accessible_resources_extensions() {
let other = ExtensionTestUtils.loadExtension({
manifest: {
applications: { gecko: { id: "other@mochitest" } },
},
files: {
"page.js": pageScript,
"page.html": `<html><head>
<meta charset="utf-8">
<script src="page.js"></script>
</head></html>`,
},
});
let extension = ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
applications: { gecko: { id: "this@mochitest" } },
web_accessible_resources: [
{
resources: ["/image.png"],
extension_ids: ["other@mochitest"],
},
],
},
files: {
"image.png": IMAGE_ARRAYBUFFER,
"inaccessible.png": IMAGE_ARRAYBUFFER,
"page.js": pageScript,
"page.html": `<html><head>
<meta charset="utf-8">
<script src="page.js"></script>
</head></html>`,
},
});
await extension.startup();
let extensionUrl = `moz-extension://${extension.uuid}/`;
await other.startup();
let pageUrl = `moz-extension://${other.uuid}/page.html`;
let page = await ExtensionTestUtils.loadContentPage(pageUrl);
await other.awaitMessage("page-loaded");
other.sendMessage({
name: "accessible resource",
elem: "img",
url: `${extensionUrl}image.png`,
shouldLoad: true,
});
await other.awaitMessage("web-accessible-resources");
other.sendMessage({
name: "inaccessible resource",
elem: "img",
url: `${extensionUrl}inaccessible.png`,
shouldLoad: false,
});
await other.awaitMessage("web-accessible-resources");
await page.close();
// test that the extension may load it's own web accessible resource
page = await ExtensionTestUtils.loadContentPage(`${extensionUrl}page.html`);
await extension.awaitMessage("page-loaded");
extension.sendMessage({
name: "accessible resource",
elem: "img",
url: `${extensionUrl}image.png`,
shouldLoad: true,
});
await extension.awaitMessage("web-accessible-resources");
await page.close();
await extension.unload();
await other.unload();
});
// test that a web page not in matches cannot load the resource
add_task(async function test_web_accessible_resources_inaccessible() {
let extension = ExtensionTestUtils.loadExtension({
temporarilyInstalled: true,
manifest: {
manifest_version: 3,
applications: { gecko: { id: "web@mochitest" } },
content_scripts: [
{
matches: ["http://example.com/data/*"],
js: ["page.js"],
run_at: "document_idle",
},
],
web_accessible_resources: [
{
resources: ["/image.png"],
extension_ids: ["some_other_ext@mochitest"],
},
],
host_permissions: ["*://example.com/*"],
granted_host_permissions: true,
},
files: {
"image.png": IMAGE_ARRAYBUFFER,
"page.js": pageScript,
"page.html": `<html><head>
<meta charset="utf-8">
<script src="page.js"></script>
</head></html>`,
},
});
await extension.startup();
let extensionUrl = `moz-extension://${extension.uuid}/`;
let page = await ExtensionTestUtils.loadContentPage(
"http://example.com/data/"
);
await extension.awaitMessage("page-loaded");
extension.sendMessage({
name: "cannot access resource",
elem: "img",
url: `${extensionUrl}image.png`,
content_context: true,
shouldLoad: false,
});
await extension.awaitMessage("web-accessible-resources");
await page.close();
await extension.unload();
});