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-08-02 17:08:58 +00:00
parent 16b2b94bd8
commit 4e3ceb6b90
11 changed files with 370 additions and 42 deletions

View File

@@ -18,6 +18,7 @@
#include "nsAboutProtocolUtils.h" #include "nsAboutProtocolUtils.h"
#include "ThirdPartyUtil.h" #include "ThirdPartyUtil.h"
#include "mozilla/ContentPrincipal.h" #include "mozilla/ContentPrincipal.h"
#include "mozilla/ExtensionPolicyService.h"
#include "mozilla/NullPrincipal.h" #include "mozilla/NullPrincipal.h"
#include "mozilla/dom/BlobURLProtocolHandler.h" #include "mozilla/dom/BlobURLProtocolHandler.h"
#include "mozilla/dom/ChromeUtils.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; bool fetchableByAnyone;
rv = NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_FETCHABLE_BY_ANYONE, rv = NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_FETCHABLE_BY_ANYONE,
&fetchableByAnyone); &fetchableByAnyone);
@@ -603,16 +606,34 @@ nsresult BasePrincipal::CheckMayLoadHelper(nsIURI* aURI,
return NS_OK; return NS_OK;
} }
if (aReport) { // Get the principal uri for the last flag check or error.
nsCOMPtr<nsIURI> prinURI; nsCOMPtr<nsIURI> prinURI;
rv = GetURI(getter_AddRefs(prinURI)); rv = GetURI(getter_AddRefs(prinURI));
if (NS_SUCCEEDED(rv) && prinURI) { if (!(NS_SUCCEEDED(rv) && prinURI)) {
nsScriptSecurityManager::ReportError( return NS_ERROR_DOM_BAD_URI;
"CheckSameOriginError", prinURI, aURI, }
mOriginAttributes.mPrivateBrowsingId > 0, aInnerWindowID);
// 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; return NS_ERROR_DOM_BAD_URI;
} }

View File

@@ -741,21 +741,6 @@ nsScriptSecurityManager::CheckLoadURIWithPrincipal(nsIPrincipal* aPrincipal,
return NS_ERROR_DOM_BAD_URI; 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 // Check for uris that are only loadable by principals that subsume them
bool targetURIIsLoadableBySubsumers = false; bool targetURIIsLoadableBySubsumers = false;
rv = NS_URIChainHasFlags(targetBaseURI, rv = NS_URIChainHasFlags(targetBaseURI,
@@ -829,6 +814,7 @@ nsScriptSecurityManager::CheckLoadURIWithPrincipal(nsIPrincipal* aPrincipal,
bool schemesMatch = bool schemesMatch =
scheme.Equals(otherScheme, nsCaseInsensitiveCStringComparator); scheme.Equals(otherScheme, nsCaseInsensitiveCStringComparator);
bool isSamePage = false; bool isSamePage = false;
bool isExtensionMismatch = false;
// about: URIs are special snowflakes. // about: URIs are special snowflakes.
if (scheme.EqualsLiteral("about") && schemesMatch) { if (scheme.EqualsLiteral("about") && schemesMatch) {
nsAutoCString moduleName, otherModuleName; 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 { } else {
bool equalExceptRef = false; bool equalExceptRef = false;
rv = currentURI->EqualsExceptRef(currentOtherURI, &equalExceptRef); 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 // 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 // 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 // from the same scheme, or this is two different extensions, check
// URI allow the current source URI to link to it. // 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. // The policy is specified by the protocol flags on both URIs.
if (!schemesMatch || (denySameSchemeLinks && !isSamePage)) { if (!schemesMatch || (denySameSchemeLinks && !isSamePage) ||
isExtensionMismatch) {
return CheckLoadURIFlags( return CheckLoadURIFlags(
currentURI, currentOtherURI, sourceBaseURI, targetBaseURI, aFlags, currentURI, currentOtherURI, sourceBaseURI, targetBaseURI, aFlags,
aPrincipal->OriginAttributesRef().mPrivateBrowsingId > 0, aPrincipal->OriginAttributesRef().mPrivateBrowsingId > 0,
@@ -936,7 +931,8 @@ nsresult nsScriptSecurityManager::CheckLoadURIFlags(
nsresult rv = aTargetBaseURI->GetScheme(targetScheme); nsresult rv = aTargetBaseURI->GetScheme(targetScheme);
if (NS_FAILED(rv)) return rv; 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, rv = DenyAccessIfURIHasFlags(aTargetURI,
nsIProtocolHandler::URI_DANGEROUS_TO_LOAD); nsIProtocolHandler::URI_DANGEROUS_TO_LOAD);
if (NS_FAILED(rv)) { 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 // Check for chrome target URI
bool targetURIIsUIResource = false; bool targetURIIsUIResource = false;
rv = NS_URIChainHasFlags(aTargetURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, rv = NS_URIChainHasFlags(aTargetURI, nsIProtocolHandler::URI_IS_UI_RESOURCE,

View File

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

View File

@@ -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. * defined in nsIProtocolHandler - if not - deny the load.
*/ */
nsresult nsContentSecurityManager::CheckChannelHasProtocolSecurityFlag( nsresult nsContentSecurityManager::CheckChannelHasProtocolSecurityFlag(
@@ -1273,6 +1273,9 @@ nsresult nsContentSecurityManager::CheckChannelHasProtocolSecurityFlag(
NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_SUCCESS(rv, rv);
uint32_t securityFlagsSet = 0; uint32_t securityFlagsSet = 0;
if (flags & nsIProtocolHandler::WEBEXT_URI_WEB_ACCESSIBLE) {
securityFlagsSet += 1;
}
if (flags & nsIProtocolHandler::URI_LOADABLE_BY_ANYONE) { if (flags & nsIProtocolHandler::URI_LOADABLE_BY_ANYONE) {
securityFlagsSet += 1; securityFlagsSet += 1;
} }

View File

@@ -275,7 +275,8 @@ interface nsIProtocolHandler : nsISupports
/** /**
* This URI may be fetched and the contents are visible to anyone. This is * 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 * 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); 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 * 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); const unsigned long WEBEXT_URI_WEB_ACCESSIBLE = (1 << 24);
}; };

View File

@@ -416,15 +416,16 @@ nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI,
URLInfo url(aURI); URLInfo url(aURI);
if (auto* policy = EPS().GetByURL(url)) { if (auto* policy = EPS().GetByURL(url)) {
// In general a moz-extension URI is only loadable by chrome, but a // In general a moz-extension URI is only loadable by chrome, but an
// whitelisted subset are web-accessible (and cross-origin fetchable). Check // allowlist subset are web-accessible (and cross-origin fetchable).
// that whitelist. For Manifest V3 extensions, an additional whitelist // The allowlist is checked using EPS.SourceMayLoadExtensionURI in
// for the source loading the url must be checked so we add the flag // BasePrincipal and nsScriptSecurityManager.
// WEBEXT_URI_WEB_ACCESSIBLE, which is then checked in
// nsScriptSecurityManager.
if (policy->IsWebAccessiblePath(url.FilePath())) { if (policy->IsWebAccessiblePath(url.FilePath())) {
flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE | if (policy->ManifestVersion() < 3) {
WEBEXT_URI_WEB_ACCESSIBLE; flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE;
} else {
flags |= WEBEXT_URI_WEB_ACCESSIBLE;
}
} else { } else {
flags |= URI_DANGEROUS_TO_LOAD; flags |= URI_DANGEROUS_TO_LOAD;
} }

View File

@@ -304,6 +304,18 @@ const POSTPROCESSORS = {
context.logError(context.makeError(msg)); context.logError(context.makeError(msg));
throw new Error(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 // Parses a regular expression, with support for the Python extended

View File

@@ -143,12 +143,24 @@ WebAccessibleResource::WebAccessibleResource(
return; return;
} }
if (aInit.mMatches.WasPassed()) { if (!aInit.mMatches.IsNull()) {
MatchPatternOptions options; MatchPatternOptions options;
options.mRestrictSchemes = true; options.mRestrictSchemes = true;
mMatches = ParseMatches(aGlobal, aInit.mMatches.Value(), options, mMatches = ParseMatches(aGlobal, aInit.mMatches.Value(), options,
ErrorBehavior::CreateEmptyPattern, aRv); 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) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAccessibleResource)

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
"use strict"; "use strict";
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
const server = createHttpServer({ hosts: ["example.com", "example.org"] }); 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)) const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0))
.buffer; .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() { add_task(async function test_web_accessible_resources() {
async function contentScript() { async function contentScript() {
let canLoad = window.location.href.startsWith("http://example.com"); let canLoad = window.location.href.startsWith("http://example.com");
let urls = [ let urls = [
{ {
name: "iframe", name: "iframe",
@@ -136,3 +204,178 @@ add_task(async function test_web_accessible_resources() {
await page.close(); await page.close();
await extension.unload(); 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();
});