mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 18:04:46 +00:00
Bug 1254194: [webext] Allow extensions to register custom content security policies. r=billm f=aswan
MozReview-Commit-ID: 8L6ZsyDjIpf --HG-- extra : rebase_source : b6ccbcf849b0e7db835d14a0ba9de588c0188869 extra : histedit_source : 7f966c1d821641fc3551dc4c508f5ce8f990d5a3%2Cafa5697b301620119147292745a2007961907fa8
This commit is contained in:
parent
bd8adfebce
commit
623a4f8665
@ -96,6 +96,10 @@ pref("extensions.systemAddon.update.url", "https://aus5.mozilla.org/update/3/Sys
|
||||
// See the SCOPE constants in AddonManager.jsm for values to use here.
|
||||
pref("extensions.autoDisableScopes", 15);
|
||||
|
||||
// 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.default-content-security-policy", "script-src 'self'; object-src 'self';");
|
||||
|
||||
// Require signed add-ons by default
|
||||
pref("xpinstall.signatures.required", true);
|
||||
pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
|
||||
|
@ -14,6 +14,25 @@
|
||||
[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 content security policy which applies to documents 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 getAddonCSP(in AString aAddonId);
|
||||
|
||||
/**
|
||||
* Returns true if unprivileged code associated with the given addon may load
|
||||
* data from |aURI|.
|
||||
|
@ -263,6 +263,10 @@ pref("services.kinto.update_enabled", true);
|
||||
/* Don't let XPIProvider install distribution add-ons; we do our own thing on mobile. */
|
||||
pref("extensions.installDistroAddons", false);
|
||||
|
||||
// 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.default-content-security-policy", "script-src 'self'; object-src 'self';");
|
||||
|
||||
/* block popups by default, and notify the user about blocked popups */
|
||||
pref("dom.disable_open_during_load", true);
|
||||
pref("privacy.popups.showBrowserMessage", true);
|
||||
|
@ -1396,13 +1396,11 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
||||
}),
|
||||
|
||||
startup() {
|
||||
try {
|
||||
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
let started = false;
|
||||
return this.readManifest().then(() => {
|
||||
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
|
||||
started = true;
|
||||
|
||||
if (!this.hasShutdown) {
|
||||
return this.initLocale();
|
||||
}
|
||||
@ -1428,7 +1426,9 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
||||
dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
|
||||
Cu.reportError(e);
|
||||
|
||||
ExtensionManagement.shutdownExtension(this.uuid);
|
||||
if (started) {
|
||||
ExtensionManagement.shutdownExtension(this.uuid);
|
||||
}
|
||||
|
||||
this.cleanupGeneratedFile();
|
||||
|
||||
|
@ -160,6 +160,7 @@ var Service = {
|
||||
this.uuidMap.set(uuid, extension);
|
||||
this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
|
||||
this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
|
||||
this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
|
||||
},
|
||||
|
||||
// Called when an extension is unloaded.
|
||||
@ -168,6 +169,7 @@ var Service = {
|
||||
this.uuidMap.delete(uuid);
|
||||
this.aps.setAddonLoadURICallback(extension.id, null);
|
||||
this.aps.setAddonLocalizeCallback(extension.id, null);
|
||||
this.aps.setAddonCSP(extension.id, null);
|
||||
|
||||
let handler = Services.io.getProtocolHandler("moz-extension");
|
||||
handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
|
||||
|
@ -9,21 +9,25 @@ const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
instanceOf,
|
||||
} = ExtensionUtils;
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
|
||||
"@mozilla.org/addons/content-policy;1",
|
||||
"nsIAddonContentPolicy");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Schemas"];
|
||||
|
||||
/* globals Schemas, URL */
|
||||
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
function readJSON(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
||||
@ -257,6 +261,14 @@ const FORMATS = {
|
||||
throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`);
|
||||
},
|
||||
|
||||
contentSecurityPolicy(string, context) {
|
||||
let error = contentPolicyService.validateAddonCSP(string);
|
||||
if (error != null) {
|
||||
throw new SyntaxError(error);
|
||||
}
|
||||
return string;
|
||||
},
|
||||
|
||||
date(string, context) {
|
||||
// A valid ISO 8601 timestamp.
|
||||
const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
|
||||
|
@ -149,6 +149,13 @@
|
||||
"items": { "$ref": "ContentScript" }
|
||||
},
|
||||
|
||||
"content_security_policy": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"format": "contentSecurityPolicy",
|
||||
"onError": "warn"
|
||||
},
|
||||
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -10,5 +10,42 @@ XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
|
||||
"resource://gre/modules/Extension.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
||||
"resource://gre/modules/Schemas.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
||||
/* exported normalizeManifest */
|
||||
|
||||
let BASE_MANIFEST = {
|
||||
"applications": {"gecko": {"id": "test@web.ext"}},
|
||||
|
||||
"manifest_version": 2,
|
||||
|
||||
"name": "name",
|
||||
"version": "0",
|
||||
};
|
||||
|
||||
function* normalizeManifest(manifest, baseManifest = BASE_MANIFEST) {
|
||||
const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
|
||||
|
||||
yield Management.lazyInit();
|
||||
|
||||
let errors = [];
|
||||
let context = {
|
||||
url: null,
|
||||
|
||||
logError: error => {
|
||||
errors.push(error);
|
||||
},
|
||||
|
||||
preprocessors: {},
|
||||
};
|
||||
|
||||
manifest = Object.assign({}, baseManifest, manifest);
|
||||
|
||||
let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
|
||||
normalized.errors = errors;
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
|
||||
const ADDON_ID = "test@web.extension";
|
||||
|
||||
const aps = Cc["@mozilla.org/addons/policy-service;1"]
|
||||
.getService(Ci.nsIAddonPolicyService).wrappedJSObject;
|
||||
|
||||
do_register_cleanup(() => {
|
||||
aps.setAddonCSP(ADDON_ID, null);
|
||||
});
|
||||
|
||||
add_task(function* test_addon_csp() {
|
||||
equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"),
|
||||
"Expected base CSP value");
|
||||
|
||||
equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"),
|
||||
"Expected default CSP value");
|
||||
|
||||
equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
|
||||
"CSP for unknown add-on ID should be the default CSP");
|
||||
|
||||
|
||||
const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'";
|
||||
|
||||
aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY);
|
||||
|
||||
equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy");
|
||||
|
||||
|
||||
aps.setAddonCSP(ADDON_ID, null);
|
||||
|
||||
equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
|
||||
"CSP should revert to default when set to null");
|
||||
});
|
@ -0,0 +1,30 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
|
||||
add_task(function* test_manifest_csp() {
|
||||
let normalized = yield normalizeManifest({
|
||||
"content_security_policy": "script-src 'self'; object-src 'none'",
|
||||
});
|
||||
|
||||
equal(normalized.error, undefined, "Should not have an error");
|
||||
equal(normalized.errors.length, 0, "Should not have warnings");
|
||||
equal(normalized.value.content_security_policy,
|
||||
"script-src 'self'; object-src 'none'",
|
||||
"Should have the expected poilcy string");
|
||||
|
||||
|
||||
normalized = yield normalizeManifest({
|
||||
"content_security_policy": "object-src 'none'",
|
||||
});
|
||||
|
||||
equal(normalized.error, undefined, "Should not have an error");
|
||||
|
||||
Assert.deepEqual(normalized.errors,
|
||||
["Error processing content_security_policy: SyntaxError: Policy is missing a required 'script-src' directive"],
|
||||
"Should have the expected warning");
|
||||
|
||||
equal(normalized.value.content_security_policy, null,
|
||||
"Invalid policy string should be omitted");
|
||||
});
|
@ -4,10 +4,12 @@ tail =
|
||||
firefox-appdir = browser
|
||||
skip-if = toolkit == 'gonk'
|
||||
|
||||
[test_csp_custom_policies.js]
|
||||
[test_csp_validator.js]
|
||||
[test_locale_data.js]
|
||||
[test_locale_converter.js]
|
||||
[test_ext_contexts.js]
|
||||
[test_ext_json_parser.js]
|
||||
[test_ext_manifest_content_security_policy.js]
|
||||
[test_ext_schemas.js]
|
||||
[test_getAPILevelForWindow.js]
|
||||
|
@ -62,14 +62,34 @@ RemoteTagServiceService.prototype = {
|
||||
function AddonPolicyService()
|
||||
{
|
||||
this.wrappedJSObject = this;
|
||||
this.cspStrings = new Map();
|
||||
this.mayLoadURICallbacks = new Map();
|
||||
this.localizeCallbacks = new Map();
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this, "baseCSP", "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:;");
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this, "defaultCSP", "extensions.webextensions.default-content-security-policy",
|
||||
"script-src 'self'; object-src 'self';");
|
||||
}
|
||||
|
||||
AddonPolicyService.prototype = {
|
||||
classID: Components.ID("{89560ed3-72e3-498d-a0e8-ffe50334d7c5}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonPolicyService]),
|
||||
|
||||
/**
|
||||
* Returns the content security policy which applies to documents 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.
|
||||
*/
|
||||
getAddonCSP(aAddonId) {
|
||||
let csp = this.cspStrings.get(aAddonId);
|
||||
return csp || this.defaultCSP;
|
||||
},
|
||||
|
||||
/*
|
||||
* Invokes a callback (if any) associated with the addon to determine whether
|
||||
* unprivileged code running within the addon is allowed to perform loads from
|
||||
@ -136,6 +156,19 @@ AddonPolicyService.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Sets the custom CSP string to be used for the add-on. Not accessible over
|
||||
* XPCOM - callers should use .wrappedJSObject on the service to call it
|
||||
* directly.
|
||||
*/
|
||||
setAddonCSP(aAddonId, aCSPString) {
|
||||
if (aCSPString) {
|
||||
this.cspStrings.set(aAddonId, aCSPString);
|
||||
} else {
|
||||
this.cspStrings.delete(aAddonId);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Sets the callbacks used by the stream converter service to localize
|
||||
* add-on resources.
|
||||
|
Loading…
Reference in New Issue
Block a user