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:
Kris Maglione 2016-04-23 21:29:15 -07:00
Родитель bd8adfebce
Коммит 623a4f8665
12 изменённых файлов: 199 добавлений и 11 удалений

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

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