Bug 1594234 manifest v3 content security policy support r=robwu,geckoview-reviewers,agi

Implement manifest v3 CSP that is compatible with the current chrome implementation.

Support for content_security_policy.isolated_world (a.k.a. content_security_policy.content_scripts)
has been removed for consistency with
345390adf6%5E%21/

Differential Revision: https://phabricator.services.mozilla.com/D100573
This commit is contained in:
Shane Caraveo 2021-01-07 14:53:18 +00:00
parent 3b83060956
commit 4a14410028
18 changed files with 388 additions and 301 deletions

View File

@ -70,6 +70,7 @@ pref("extensions.geckoProfiler.acceptedExtensionIds", "geckoprofiler@mozilla.com
// Add-on content security policies.
pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
pref("extensions.webextensions.base-content-security-policy.v3", "script-src 'self'; object-src 'self'; style-src 'self'; worker-src 'self';");
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
pref("extensions.webextensions.remote", true);

View File

@ -14,18 +14,17 @@
[scriptable, uuid(8a034ef9-9d14-4c5d-8319-06c1ab574baa)]
interface nsIAddonPolicyService : nsISupports
{
/**
* Returns the base content security policy, which is applied to all
* extension documents, in addition to any custom policies.
*/
readonly attribute AString baseCSP;
/**
* Returns the default content security policy which applies to extension
* documents which do not specify any custom policies.
*/
readonly attribute AString defaultCSP;
/**
* Returns the base content security policy which applies to all extension resources.
*/
AString getBaseCSP(in AString aAddonId);
/**
* Returns the content security policy which applies to documents belonging
* to the extension with the given ID. This may be either a custom policy,
@ -33,13 +32,6 @@ interface nsIAddonPolicyService : nsISupports
*/
AString getExtensionPageCSP(in AString aAddonId);
/**
* Returns the content security policy which applies to content scripts belonging
* to the extension with the given ID. This may be either a custom policy,
* if one was supplied, or the default policy if one was not.
*/
AString getContentScriptCSP(in AString aAddonId);
/**
* Returns the generated background page as a data-URI, if any. If the addon
* does not have an auto-generated background page, an empty string is

View File

@ -3642,10 +3642,7 @@ nsresult Document::InitCSP(nsIChannel* aChannel) {
// ----- if the doc is an addon, apply its CSP.
if (addonPolicy) {
nsAutoString extensionPageCSP;
Unused << ExtensionPolicyService::GetSingleton().GetBaseCSP(
extensionPageCSP);
mCSP->AppendPolicy(extensionPageCSP, false, false);
mCSP->AppendPolicy(addonPolicy->BaseCSP(), false, false);
mCSP->AppendPolicy(addonPolicy->ExtensionPageCSP(), false, false);
// Bug 1548468: Move CSP off ExpandedPrincipal

View File

@ -49,6 +49,21 @@ interface WebExtensionPolicy {
[Constant]
readonly attribute boolean isPrivileged;
/**
* The manifest version in use by the extension.
*/
[Constant]
readonly attribute unsigned long manifestVersion;
/**
* The base content security policy string to apply on extension
* pages for this extension. The baseCSP is specific to the
* manifest version. If the manifest version is 3 or higher it
* is also applied to content scripts.
*/
[Constant]
readonly attribute DOMString baseCSP;
/**
* The content security policy string to apply to all pages loaded from the
* extension's moz-extension: protocol. If one is not provided by the
@ -58,18 +73,6 @@ interface WebExtensionPolicy {
[Constant]
readonly attribute DOMString extensionPageCSP;
/**
* The content security policy string to apply to all the content scripts
* belonging to the extension. If one is not provided by the
* extension the default value from preferences is used.
* See extensions.webextensions.default-content-security-policy.
*
* This is currently disabled, see bug 1578284. Developers may enable it
* for testing using extensions.content_script_csp.enabled.
*/
[Constant]
readonly attribute DOMString contentScriptCSP;
/**
* The list of currently-active permissions for the extension, as specified
* in its manifest.json file. May be updated to reflect changes in the
@ -276,8 +279,9 @@ dictionary WebExtensionInit {
sequence<WebExtensionContentScriptInit> contentScripts = [];
// The use of a content script csp is determined by the manifest version.
unsigned long manifestVersion = 2;
DOMString? extensionPageCSP = null;
DOMString? contentScriptCSP = null;
sequence<DOMString>? backgroundScripts = null;
DOMString? backgroundWorkerScript = null;

View File

@ -1128,6 +1128,11 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) {
if (!addonPolicy) {
return NS_OK;
}
// For backwards compatibility, content scripts have no CSP
// in manifest v2. Only apply content script CSP to V3 or later.
if (addonPolicy->ManifestVersion() < 3) {
return NS_OK;
}
nsString url;
MOZ_TRY_VAR(url, addonPolicy->GetURL(u""_ns));
@ -1135,9 +1140,7 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) {
nsCOMPtr<nsIURI> selfURI;
MOZ_TRY(NS_NewURI(getter_AddRefs(selfURI), url));
nsAutoString baseCSP;
MOZ_ALWAYS_SUCCEEDS(
ExtensionPolicyService::GetSingleton().GetBaseCSP(baseCSP));
const nsAString& baseCSP = addonPolicy->BaseCSP();
// If we got here, we're definitly an expanded principal.
auto expanded = basePrin->As<ExpandedPrincipal>();
@ -1167,10 +1170,6 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) {
MOZ_TRY(csp->AppendPolicy(baseCSP, reportOnly, false));
// Set default or extension provided csp.
const nsAString& contentScriptCSP = addonPolicy->ContentScriptCSP();
MOZ_TRY(csp->AppendPolicy(contentScriptCSP, reportOnly, false));
expanded->SetCsp(csp);
return NS_OK;
}

View File

@ -198,6 +198,7 @@ pref("extensions.webextOptionalPermissionPrompts", true);
// Add-on content security policies.
pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
pref("extensions.webextensions.base-content-security-policy.v3", "script-src 'self'; object-src 'self'; style-src 'self'; worker-src 'self';");
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
pref("extensions.webextensions.background-delayed-startup", true);

View File

@ -2182,8 +2182,14 @@ class Extension extends ExtensionData {
return manifest;
}
get manifestVersion() {
return this.manifest.manifest_version;
}
get extensionPageCSP() {
const { content_security_policy } = this.manifest;
// While only manifest v3 should contain an object,
// we'll remain lenient here.
if (
content_security_policy &&
typeof content_security_policy === "object"
@ -2193,19 +2199,6 @@ class Extension extends ExtensionData {
return content_security_policy;
}
get contentScriptCSP() {
let { content_security_policy } = this.manifest;
if (
content_security_policy &&
typeof content_security_policy === "object"
) {
return (
content_security_policy.content_scripts ||
content_security_policy.isolated_world
);
}
}
get backgroundScripts() {
return this.manifest.background?.scripts;
}
@ -2234,8 +2227,8 @@ class Extension extends ExtensionData {
id: this.id,
uuid: this.uuid,
name: this.name,
manifestVersion: this.manifestVersion,
extensionPageCSP: this.extensionPageCSP,
contentScriptCSP: this.contentScriptCSP,
instanceId: this.instanceId,
resourceURL: this.resourceURL,
contentScripts: this.contentScripts,

View File

@ -47,12 +47,6 @@ using dom::ContentFrameMessageManager;
using dom::Document;
using dom::Promise;
#define BASE_CSP_PREF "extensions.webextensions.base-content-security-policy"
#define DEFAULT_BASE_CSP \
"script-src 'self' https://* moz-extension: blob: filesystem: " \
"'unsafe-eval' 'unsafe-inline'; " \
"object-src 'self' https://* moz-extension: blob: filesystem:;"
#define DEFAULT_CSP_PREF \
"extensions.webextensions.default-content-security-policy"
#define DEFAULT_DEFAULT_CSP "script-src 'self'; object-src 'self';"
@ -94,7 +88,6 @@ ExtensionPolicyService::ExtensionPolicyService() {
mObs = services::GetObserverService();
MOZ_RELEASE_ASSERT(mObs);
mBaseCSP.SetIsVoid(true);
mDefaultCSP.SetIsVoid(true);
RegisterObservers();
@ -228,7 +221,6 @@ void ExtensionPolicyService::RegisterObservers() {
mObs->AddObserver(this, "document-on-opening-request", false);
}
Preferences::AddStrongObserver(this, BASE_CSP_PREF);
Preferences::AddStrongObserver(this, DEFAULT_CSP_PREF);
}
@ -240,7 +232,6 @@ void ExtensionPolicyService::UnregisterObservers() {
mObs->RemoveObserver(this, "document-on-opening-request");
}
Preferences::RemoveObserver(this, BASE_CSP_PREF);
Preferences::RemoveObserver(this, DEFAULT_CSP_PREF);
}
@ -268,9 +259,7 @@ nsresult ExtensionPolicyService::Observe(nsISupports* aSubject,
} else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
const nsCString converted = NS_ConvertUTF16toUTF8(aData);
const char* pref = converted.get();
if (!strcmp(pref, BASE_CSP_PREF)) {
mBaseCSP.SetIsVoid(true);
} else if (!strcmp(pref, DEFAULT_CSP_PREF)) {
if (!strcmp(pref, DEFAULT_CSP_PREF)) {
mDefaultCSP.SetIsVoid(true);
}
}
@ -550,19 +539,6 @@ void ExtensionPolicyService::CheckContentScripts(const DocInfo& aDocInfo,
* nsIAddonPolicyService
*****************************************************************************/
nsresult ExtensionPolicyService::GetBaseCSP(nsAString& aBaseCSP) {
if (mBaseCSP.IsVoid()) {
nsresult rv = Preferences::GetString(BASE_CSP_PREF, mBaseCSP);
if (NS_FAILED(rv)) {
mBaseCSP.AssignLiteral(DEFAULT_BASE_CSP);
}
mBaseCSP.SetIsVoid(false);
}
aBaseCSP.Assign(mBaseCSP);
return NS_OK;
}
nsresult ExtensionPolicyService::GetDefaultCSP(nsAString& aDefaultCSP) {
if (mDefaultCSP.IsVoid()) {
nsresult rv = Preferences::GetString(DEFAULT_CSP_PREF, mDefaultCSP);
@ -576,19 +552,19 @@ nsresult ExtensionPolicyService::GetDefaultCSP(nsAString& aDefaultCSP) {
return NS_OK;
}
nsresult ExtensionPolicyService::GetExtensionPageCSP(const nsAString& aAddonId,
nsAString& aResult) {
nsresult ExtensionPolicyService::GetBaseCSP(const nsAString& aAddonId,
nsAString& aResult) {
if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
policy->GetExtensionPageCSP(aResult);
policy->GetBaseCSP(aResult);
return NS_OK;
}
return NS_ERROR_INVALID_ARG;
}
nsresult ExtensionPolicyService::GetContentScriptCSP(const nsAString& aAddonId,
nsresult ExtensionPolicyService::GetExtensionPageCSP(const nsAString& aAddonId,
nsAString& aResult) {
if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
policy->GetContentScriptCSP(aResult);
policy->GetExtensionPageCSP(aResult);
return NS_OK;
}
return NS_ERROR_INVALID_ARG;

View File

@ -123,7 +123,6 @@ class ExtensionPolicyService final : public nsIAddonPolicyService,
nsCOMPtr<nsIObserverService> mObs;
nsString mBaseCSP;
nsString mDefaultCSP;
};

View File

@ -222,8 +222,8 @@ ExtensionManager = {
allowedOrigins: extension.allowedOrigins,
webAccessibleResources: extension.webAccessibleResources,
manifestVersion: extension.manifestVersion,
extensionPageCSP: extension.extensionPageCSP,
contentScriptCSP: extension.contentScriptCSP,
localizeCallback,

View File

@ -290,6 +290,19 @@ const POSTPROCESSORS = {
context.logError(context.makeError(msg));
throw new Error(msg);
},
manifestVersionCheck(value, context) {
if (
value == 2 ||
(value == 3 &&
Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
) {
return value;
}
const msg = `Unsupported manifest version: ${value}`;
context.logError(context.makeError(msg));
throw new Error(msg);
},
};
// Parses a regular expression, with support for the Python extended
@ -1088,11 +1101,11 @@ const FORMATS = {
contentSecurityPolicy(string, context) {
let error = contentPolicyService.validateAddonCSP(string);
if (error != null) {
// The SyntaxError raised below is not reported as part of the "choices" error message,
// The CSP validation error is not reported as part of the "choices" error message,
// we log the CSP validation error explicitly here to make it easier for the addon developers
// to see and fix the extension CSP.
context.logError(`Error processing ${context.currentTarget}: ${error}`);
throw new SyntaxError(error);
return null;
}
return string;
},

View File

@ -41,6 +41,18 @@ static const char kBackgroundPageHTMLEnd[] =
</body>\n\
</html>";
#define BASE_CSP_PREF_V2 "extensions.webextensions.base-content-security-policy"
#define DEFAULT_BASE_CSP_V2 \
"script-src 'self' https://* moz-extension: blob: filesystem: " \
"'unsafe-eval' 'unsafe-inline'; " \
"object-src 'self' https://* moz-extension: blob: filesystem:;"
#define BASE_CSP_PREF_V3 \
"extensions.webextensions.base-content-security-policy.v3"
#define DEFAULT_BASE_CSP_V3 \
"script-src 'self'; object-src 'self'; " \
"style-src 'self'; worker-src 'self';"
static const char kRestrictedDomainPref[] =
"extensions.webextensions.restrictedDomains";
@ -133,8 +145,8 @@ WebExtensionPolicy::WebExtensionPolicy(GlobalObject& aGlobal,
: mId(NS_AtomizeMainThread(aInit.mId)),
mHostname(aInit.mMozExtensionHostname),
mName(aInit.mName),
mManifestVersion(aInit.mManifestVersion),
mExtensionPageCSP(aInit.mExtensionPageCSP),
mContentScriptCSP(aInit.mContentScriptCSP),
mLocalizeCallback(aInit.mLocalizeCallback),
mIsPrivileged(aInit.mIsPrivileged),
mPermissions(new AtomSet(aInit.mPermissions)) {
@ -165,14 +177,12 @@ WebExtensionPolicy::WebExtensionPolicy(GlobalObject& aGlobal,
mBackgroundWorkerScript.Assign(aInit.mBackgroundWorkerScript);
}
InitializeBaseCSP();
if (mExtensionPageCSP.IsVoid()) {
EPS().GetDefaultCSP(mExtensionPageCSP);
}
if (mContentScriptCSP.IsVoid()) {
EPS().GetDefaultCSP(mContentScriptCSP);
}
mContentScripts.SetCapacity(aInit.mContentScripts.Length());
for (const auto& scriptInit : aInit.mContentScripts) {
// The activeTab permission is only for dynamically injected scripts,
@ -210,6 +220,21 @@ already_AddRefed<WebExtensionPolicy> WebExtensionPolicy::Constructor(
return policy.forget();
}
void WebExtensionPolicy::InitializeBaseCSP() {
if (mManifestVersion < 3) {
nsresult rv = Preferences::GetString(BASE_CSP_PREF_V2, mBaseCSP);
if (NS_FAILED(rv)) {
mBaseCSP.AssignLiteral(DEFAULT_BASE_CSP_V2);
}
return;
}
// Version 3 or higher.
nsresult rv = Preferences::GetString(BASE_CSP_PREF_V3, mBaseCSP);
if (NS_FAILED(rv)) {
mBaseCSP.AssignLiteral(DEFAULT_BASE_CSP_V3);
}
}
/* static */
void WebExtensionPolicy::GetActiveExtensions(
dom::GlobalObject& aGlobal,

View File

@ -99,11 +99,13 @@ class WebExtensionPolicy final : public nsISupports,
const nsString& Name() const { return mName; }
void GetName(nsAString& aName) const { aName = mName; }
uint32_t ManifestVersion() const { return mManifestVersion; }
const nsString& ExtensionPageCSP() const { return mExtensionPageCSP; }
void GetExtensionPageCSP(nsAString& aCSP) const { aCSP = mExtensionPageCSP; }
const nsString& ContentScriptCSP() const { return mContentScriptCSP; }
void GetContentScriptCSP(nsAString& aCSP) const { aCSP = mContentScriptCSP; }
const nsString& BaseCSP() const { return mBaseCSP; }
void GetBaseCSP(nsAString& aCSP) const { aCSP = mBaseCSP; }
already_AddRefed<MatchPatternSet> AllowedOrigins() {
return do_AddRef(mHostPermissions);
@ -180,6 +182,7 @@ class WebExtensionPolicy final : public nsISupports,
bool Enable();
bool Disable();
void InitializeBaseCSP();
nsCOMPtr<nsISupports> mParent;
@ -188,8 +191,9 @@ class WebExtensionPolicy final : public nsISupports,
nsCOMPtr<nsIURI> mBaseURI;
nsString mName;
uint32_t mManifestVersion = 2;
nsString mExtensionPageCSP;
nsString mContentScriptCSP;
nsString mBaseCSP;
uint64_t mBrowsingContextGroupId = 0;

View File

@ -11,7 +11,8 @@
"manifest_version": {
"type": "integer",
"minimum": 2,
"maximum": 2
"maximum": 3,
"postprocess": "manifestVersionCheck"
},
"applications": {
@ -203,18 +204,6 @@
"optional": true,
"format": "contentSecurityPolicy",
"description": "The Content Security Policy used for extension pages."
},
"content_scripts": {
"type": "string",
"optional": true,
"format": "contentSecurityPolicy",
"description": "The Content Security Policy used for content scripts."
},
"isolated_world": {
"type": "string",
"optional": true,
"format": "contentSecurityPolicy",
"description": "An alias for content_scripts to support Chrome compatibility. Content Security Policy implementations may differ between Firefox and Chrome. If both isolated_world and content_scripts exist, the value from content_scripts will be used."
}
}
}

View File

@ -12,39 +12,27 @@ const aps = Cc["@mozilla.org/addons/policy-service;1"].getService(
Ci.nsIAddonPolicyService
);
let policy = null;
const v2_csp = Preferences.get(
"extensions.webextensions.base-content-security-policy"
);
const v3_csp = Preferences.get(
"extensions.webextensions.base-content-security-policy.v3"
);
function setExtensionCSP(csp) {
if (policy) {
policy.active = false;
}
policy = new WebExtensionPolicy({
id: ADDON_ID,
mozExtensionHostname: ADDON_ID,
baseURL: "file:///",
allowedOrigins: new MatchPatternSet([]),
localizeCallback() {},
extensionPageCSP: csp,
contentScriptCSP: csp,
});
policy.active = true;
}
registerCleanupFunction(() => {
policy.active = false;
add_task(async function test_invalid_addon_csp() {
await Assert.throws(
() => aps.getBaseCSP("invalid@missing"),
/NS_ERROR_ILLEGAL_VALUE/,
"no base csp for non-existent addon"
);
await Assert.throws(
() => aps.getExtensionPageCSP("invalid@missing"),
/NS_ERROR_ILLEGAL_VALUE/,
"no extension page csp for non-existent addon"
);
});
add_task(async function test_addon_csp() {
equal(
aps.baseCSP,
Preferences.get("extensions.webextensions.base-content-security-policy"),
"Expected base CSP value"
);
add_task(async function test_policy_csp() {
equal(
aps.defaultCSP,
Preferences.get("extensions.webextensions.default-content-security-policy"),
@ -54,101 +42,177 @@ add_task(async function test_addon_csp() {
const CUSTOM_POLICY =
"script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'";
setExtensionCSP(CUSTOM_POLICY);
let tests = [
{
name: "manifest version 2, no custom policy",
policyData: {},
expectedPolicy: aps.defaultCSP,
},
{
name: "manifest version 2, no custom policy",
policyData: {
manifestVersion: 2,
},
expectedPolicy: aps.defaultCSP,
},
{
name: "version 2 custom extension policy",
policyData: {
extensionPageCSP: CUSTOM_POLICY,
},
expectedPolicy: CUSTOM_POLICY,
},
{
name: "manifest version 2 set, custom extension policy",
policyData: {
manifestVersion: 2,
extensionPageCSP: CUSTOM_POLICY,
},
expectedPolicy: CUSTOM_POLICY,
},
{
name: "manifest version 3, no custom policy",
policyData: {
manifestVersion: 3,
},
expectedPolicy: aps.defaultCSP,
},
{
name: "manifest 3 version set, custom extensionPage policy",
policyData: {
manifestVersion: 3,
extensionPageCSP: CUSTOM_POLICY,
},
expectedPolicy: CUSTOM_POLICY,
},
];
equal(
aps.getExtensionPageCSP(ADDON_ID),
CUSTOM_POLICY,
"CSP should point to add-on's custom extension page policy"
);
let policy = null;
equal(
aps.getContentScriptCSP(ADDON_ID),
CUSTOM_POLICY,
"CSP should point to add-on's custom content script policy"
);
function setExtensionCSP({ manifestVersion, extensionPageCSP }) {
if (policy) {
policy.active = false;
}
setExtensionCSP(null);
policy = new WebExtensionPolicy({
id: ADDON_ID,
mozExtensionHostname: ADDON_ID,
baseURL: "file:///",
equal(
aps.getExtensionPageCSP(ADDON_ID),
aps.defaultCSP,
"extension page CSP should be default when set to null"
);
allowedOrigins: new MatchPatternSet([]),
localizeCallback() {},
equal(
aps.getContentScriptCSP(ADDON_ID),
aps.defaultCSP,
"content script CSP should be default when set to null"
);
manifestVersion,
extensionPageCSP,
});
policy.active = true;
}
for (let test of tests) {
info(test.name);
setExtensionCSP(test.policyData);
equal(
aps.getBaseCSP(ADDON_ID),
test.policyData.manifestVersion == 3 ? v3_csp : v2_csp,
"baseCSP is correct"
);
equal(
aps.getExtensionPageCSP(ADDON_ID),
test.expectedPolicy,
"extensionPageCSP is correct"
);
}
});
add_task(async function test_invalid_csp() {
let defaultPolicy = Preferences.get(
"extensions.webextensions.default-content-security-policy"
);
add_task(async function test_extension_csp() {
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
ExtensionTestUtils.failOnSchemaWarnings(false);
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_security_policy: {
extension_pages: `script-src 'none'`,
content_scripts: `script-src 'none'`,
let extension_pages = "script-src 'self'; object-src 'none'; img-src 'none'";
let tests = [
{
name: "manifest_v2 invalid csp results in default csp used",
manifest: {
content_security_policy: `script-src 'none'`,
},
expectedPolicy: aps.defaultCSP,
},
});
await extension.startup();
let policy = WebExtensionPolicy.getByID(extension.id);
equal(
policy.extensionPageCSP,
defaultPolicy,
"csp is default when invalid csp is provided."
);
equal(
policy.contentScriptCSP,
defaultPolicy,
"csp is default when invalid csp is provided."
);
await extension.unload();
{
name: "manifest_v3 invalid csp results in default csp used",
manifest: {
manifest_version: 3,
content_security_policy: {
extension_pages: `script-src 'none'`,
},
},
expectedPolicy: aps.defaultCSP,
},
{
name: "manifest_v2 csp",
manifest: {
manifest_version: 2,
content_security_policy: extension_pages,
},
expectedPolicy: extension_pages,
},
{
name: "manifest_v2 with no csp, expect default",
manifest: {
manifest_version: 2,
},
expectedPolicy: aps.defaultCSP,
},
{
name: "manifest_v3 used with no csp, expect default",
manifest: {
manifest_version: 3,
},
expectedPolicy: aps.defaultCSP,
},
{
name: "manifest_v3 used with v2 syntax",
manifest: {
manifest_version: 3,
content_security_policy: extension_pages,
},
expectedPolicy: extension_pages,
},
{
name: "manifest_v3 syntax used",
manifest: {
manifest_version: 3,
content_security_policy: {
extension_pages,
},
},
expectedPolicy: extension_pages,
},
];
for (let test of tests) {
info(test.name);
let extension = ExtensionTestUtils.loadExtension({
manifest: test.manifest,
});
await extension.startup();
let policy = WebExtensionPolicy.getByID(extension.id);
equal(
policy.baseCSP,
test.manifest.manifest_version == 3 ? v3_csp : v2_csp,
"baseCSP is correct"
);
equal(
policy.extensionPageCSP,
test.expectedPolicy,
"extensionPageCSP is correct."
);
await extension.unload();
}
ExtensionTestUtils.failOnSchemaWarnings(true);
});
add_task(async function test_isolated_world() {
const test_policy = "script-src 'self'; object-src 'none'; img-src 'none'";
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_security_policy: {
isolated_world: test_policy,
},
},
});
await extension.startup();
let policy = WebExtensionPolicy.getByID(extension.id);
equal(
policy.contentScriptCSP,
test_policy,
"csp is is correct when using isolated_world."
);
await extension.unload();
});
// If both isolated_world and content_scripts is provided, content_scripts is used.
add_task(async function test_isolated_world_overridden() {
const test_policy =
"script-src 'self'; object-src 'none'; img-src https://xpcshell.test.custom.csp";
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_security_policy: {
content_scripts: test_policy,
isolated_world: "script-src 'self'; object-src 'none'; img-src 'none'",
},
},
});
await extension.startup();
let policy = WebExtensionPolicy.getByID(extension.id);
equal(
policy.contentScriptCSP,
test_policy,
"csp is is correct when using isolated_world and content_scripts."
);
await extension.unload();
Services.prefs.clearUserPref("extensions.manifestV3.enabled");
});

View File

@ -9,6 +9,7 @@ const { TestUtils } = ChromeUtils.import(
// Enable and turn off report-only so we can validate the results.
Services.prefs.setBoolPref("extensions.content_script_csp.enabled", true);
Services.prefs.setBoolPref("extensions.content_script_csp.report_only", false);
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
const server = createHttpServer({
hosts: ["example.com", "csplog.example.net"],
@ -42,7 +43,6 @@ const BASE_URL = `http://example.com`;
const pageURL = `${BASE_URL}/plain.html`;
const CSP_REPORT_PATH = "/csp-report.sjs";
const CSP_REPORT = `report-uri http://csplog.example.net${CSP_REPORT_PATH};`;
function readUTF8InputStream(stream) {
let buffer = NetUtil.readInputStream(stream, stream.available());
@ -107,6 +107,21 @@ async function testFunction(data = {}) {
return 0;
}
}
function testScriptTag(data) {
return new Promise(resolve => {
let script = document.createElement("script");
script.src = data.url;
script.onload = () => {
resolve(true);
};
script.onerror = () => {
resolve(false);
};
document.body.appendChild(script);
});
}
// If the violation source is the extension the securitypolicyviolation event is not fired.
// If the page is the source, the event is fired and both the content script or page scripts
// will receive the event. If we're expecting a moz-extension report we'll fail in the
@ -128,9 +143,6 @@ function contentScript(report) {
});
}
const gDefaultContentScriptCSP =
"default-src 'self' 'report-sample'; object-src 'self'; script-src 'self';";
let TESTS = [
// Image Tests
{
@ -141,20 +153,6 @@ let TESTS = [
data: { image_url: `${BASE_URL}/data/file_image_good.png` },
expect: true,
},
{
description:
"Image from content script using extension csp. Image is not allowed.",
pageCSP: `${gDefaultCSP} img-src 'self';`,
scriptCSP: `${gDefaultContentScriptCSP} img-src 'none';`,
script: testImage,
data: { image_url: `${BASE_URL}/data/file_image_good.png` },
expect: false,
report: {
"blocked-uri": `${BASE_URL}/data/file_image_good.png`,
"document-uri": "moz-extension",
"violated-directive": "img-src",
},
},
// Fetch Tests
{
description: "Fetch url in content script uses default extension csp.",
@ -163,19 +161,6 @@ let TESTS = [
data: { url: `${BASE_URL}/data/file_image_good.png` },
expect: true,
},
{
description: "Fetch url in content script uses extension csp.",
pageCSP: `${gDefaultCSP} connect-src 'none';`,
script: testFetch,
scriptCSP: `${gDefaultContentScriptCSP} connect-src 'none';`,
data: { url: `${BASE_URL}/data/file_image_good.png` },
expect: false,
report: {
"blocked-uri": `${BASE_URL}/data/file_image_good.png`,
"document-uri": "moz-extension",
"violated-directive": "connect-src",
},
},
{
description: "Fetch full url from content script uses page csp.",
pageCSP: `${gDefaultCSP} connect-src 'none';`,
@ -195,7 +180,7 @@ let TESTS = [
description: "Fetch url from content script uses page csp.",
pageCSP: `${gDefaultCSP} connect-src *;`,
script: testFetch,
scriptCSP: `${gDefaultContentScriptCSP} connect-src 'none' 'report-sample';`,
version: 3,
data: {
content: true,
url: `${BASE_URL}/data/file_image_good.png`,
@ -203,7 +188,7 @@ let TESTS = [
expect: true,
},
// TODO Bug 1587939: Eval tests.
// Eval tests.
{
description: "Eval from content script uses page csp with unsafe-eval.",
pageCSP: `default-src 'none'; script-src 'unsafe-eval';`,
@ -214,7 +199,7 @@ let TESTS = [
{
description: "Eval from content script uses page csp.",
pageCSP: `default-src 'self' 'report-sample'; script-src 'self';`,
scriptCSP: `object-src 'self'; script-src 'self' 'unsafe-eval';`,
version: 3,
script: testEval,
data: { content: true },
expect: false,
@ -225,18 +210,40 @@ let TESTS = [
},
},
{
description: "Eval in content script uses extension csp.",
description: "Eval in content script allowed by v2 csp.",
pageCSP: `script-src 'self' 'unsafe-eval';`,
script: testEval,
expect: true,
},
{
description: "Eval in content script disallowed by v3 csp.",
pageCSP: `script-src 'self' 'unsafe-eval';`,
version: 3,
script: testEval,
expect: false,
},
{
description: "Eval in content script uses extension csp. unsafe-eval",
pageCSP: `default-src 'self'; script-src 'self';`,
scriptCSP: `object-src 'self'; script-src 'self' 'unsafe-eval';`,
script: testEval,
description: "Wrapped Eval in content script uses page csp.",
pageCSP: `script-src 'self' 'unsafe-eval';`,
version: 3,
script: async () => {
return window.wrappedJSObject.eval("true");
},
expect: true,
},
{
description: "Wrapped Eval in content script denied by page csp.",
pageCSP: `script-src 'self';`,
version: 3,
script: async () => {
try {
return window.wrappedJSObject.eval("true");
} catch (e) {
return false;
}
},
expect: false,
},
{
description: "Function from content script uses page csp.",
@ -248,7 +255,7 @@ let TESTS = [
{
description: "Function from content script uses page csp.",
pageCSP: `default-src 'self' 'report-sample'; script-src 'self';`,
scriptCSP: `object-src 'self'; script-src 'self' 'unsafe-eval';`,
version: 3,
script: testFunction,
data: { content: true },
expect: 0,
@ -259,18 +266,34 @@ let TESTS = [
},
},
{
description: "Function in content script uses default extension csp.",
description: "Function in content script uses extension csp.",
pageCSP: `default-src 'self'; script-src 'self' 'unsafe-eval';`,
version: 3,
script: testFunction,
expect: 0,
},
// The javascript url tests are not included as we do not execute those,
// aparently even with the urlbar filtering pref flipped.
// (browser.urlbar.filter.javascript)
// https://bugzilla.mozilla.org/show_bug.cgi?id=866522
// script tag injection tests
{
description:
"Function in content script uses extension csp, with unsafe-eval",
pageCSP: `default-src 'self'; script-src 'self';`,
scriptCSP: `default-src 'self'; object-src 'self'; script-src 'self' 'unsafe-eval';`,
script: testFunction,
expect: 2,
description: "remote script in content script passes in v2",
version: 2,
pageCSP: "script-src http://example.com:*;",
script: testScriptTag,
data: { url: `${BASE_URL}/data/file_script_good.js` },
expect: true,
},
{
description: "remote script in content script fails in v3",
version: 3,
pageCSP: "script-src http://example.com:*;",
script: testScriptTag,
data: { url: `${BASE_URL}/data/file_script_good.js` },
expect: false,
},
];
@ -279,6 +302,7 @@ async function runCSPTest(test) {
gCSP = `${test.pageCSP || gDefaultCSP} report-uri ${CSP_REPORT_PATH}`;
let data = {
manifest: {
manifest_version: test.version || 2,
content_scripts: [
{
matches: ["http://*/plain.html"],
@ -300,17 +324,14 @@ async function runCSPTest(test) {
`,
},
};
if (test.scriptCSP) {
data.manifest.content_security_policy = {
content_scripts: `${test.scriptCSP} ${CSP_REPORT}`,
};
}
let extension = ExtensionTestUtils.loadExtension(data);
await extension.startup();
let reportPromise = test.report && promiseCSPReport();
let contentPage = await ExtensionTestUtils.loadContentPage(pageURL);
info(`running: ${test.description}`);
await extension.awaitMessage("violationEvent");
let result = await extension.awaitMessage("result");
equal(result, test.expect, test.description);

View File

@ -2,6 +2,8 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
add_task(async function test_manifest_csp() {
let normalized = await ExtensionTestUtils.normalizeManifest({
content_security_policy: "script-src 'self'; object-src 'none'",
@ -27,7 +29,6 @@ add_task(async function test_manifest_csp() {
normalized.errors,
[
"Error processing content_security_policy: Policy is missing a required script-src directive",
'Error processing content_security_policy: Value "object-src \'none\'" must either: match the format "contentSecurityPolicy", or be an object value',
],
"Should have the expected warning"
);
@ -41,9 +42,9 @@ add_task(async function test_manifest_csp() {
add_task(async function test_manifest_csp_v3() {
let normalized = await ExtensionTestUtils.normalizeManifest({
manifest_version: 3,
content_security_policy: {
extension_pages: "script-src 'self' 'unsafe-eval'; object-src 'none'",
content_scripts: "script-src 'self'; object-src 'none'; img-src 'none'",
},
});
@ -54,14 +55,10 @@ add_task(async function test_manifest_csp_v3() {
"script-src 'self' 'unsafe-eval'; object-src 'none'",
"Should have the expected policy string"
);
equal(
normalized.value.content_security_policy.content_scripts,
"script-src 'self'; object-src 'none'; img-src 'none'",
"Should have the expected policy string"
);
ExtensionTestUtils.failOnSchemaWarnings(false);
normalized = await ExtensionTestUtils.normalizeManifest({
manifest_version: 3,
content_security_policy: {
extension_pages: "object-src 'none'",
},
@ -69,32 +66,12 @@ add_task(async function test_manifest_csp_v3() {
ExtensionTestUtils.failOnSchemaWarnings(true);
equal(normalized.error, undefined, "Should not have an error");
equal(normalized.errors.length, 2, "Should have warnings");
equal(normalized.errors.length, 1, "Should have warnings");
Assert.deepEqual(
normalized.errors,
[
"Error processing content_security_policy.extension_pages: Policy is missing a required script-src directive",
'Error processing content_security_policy: Value must either: be a string value, or .extension_pages must match the format "contentSecurityPolicy"',
],
"Should have the expected warning for extension_pages CSP"
);
ExtensionTestUtils.failOnSchemaWarnings(false);
normalized = await ExtensionTestUtils.normalizeManifest({
content_security_policy: {
content_scripts: "object-src 'none'",
},
});
ExtensionTestUtils.failOnSchemaWarnings(true);
equal(normalized.error, undefined, "Should not have an error");
equal(normalized.errors.length, 2, "Should have warnings");
Assert.deepEqual(
normalized.errors,
[
"Error processing content_security_policy.content_scripts: Policy is missing a required script-src directive",
'Error processing content_security_policy: Value must either: be a string value, or .content_scripts must match the format "contentSecurityPolicy"',
],
"Should have the expected warning for content_scripts CSP"
);
});

View File

@ -17,6 +17,38 @@ add_task(async function test_simple() {
await extension.unload();
});
add_task(async function test_manifest_V3_disabled() {
Services.prefs.setBoolPref("extensions.manifestV3.enabled", false);
let extensionData = {
manifest: {
manifest_version: 3,
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await Assert.rejects(
extension.startup(),
/Unsupported manifest version: 3/,
"manifest V3 cannot be loaded"
);
Services.prefs.clearUserPref("extensions.manifestV3.enabled");
});
add_task(async function test_manifest_V3_enabled() {
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
let extensionData = {
manifest: {
manifest_version: 3,
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
equal(extension.extension.manifest.manifest_version, 3, "manifest V3 loads");
await extension.unload();
Services.prefs.clearUserPref("extensions.manifestV3.enabled");
});
add_task(async function test_background() {
function background() {
browser.test.log("running background script");