From d1682407d270518efdbbb54dd4dcae977b17f87e Mon Sep 17 00:00:00 2001 From: Luca Greco Date: Fri, 22 Jan 2016 06:02:00 -0500 Subject: [PATCH] Bug 1214658 - Enable content script APIs into sub-frames pointed to a valid add-on url. r=kmag --- toolkit/components/extensions/Extension.jsm | 20 +-- .../extensions/ExtensionContent.jsm | 130 ++++++++++++++---- .../extensions/ExtensionManagement.jsm | 57 +++++++- 3 files changed, 165 insertions(+), 42 deletions(-) diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index a38d13c39331..3985cb0359c0 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -363,12 +363,16 @@ GlobalManager = { Schemas.inject(chromeObj, schemaWrapper); }; - // Find the add-on associated with this document via the - // principal's originAttributes. This value is computed by - // extensionURIToAddonID, which ensures that we don't inject our - // API into webAccessibleResources or remote web pages. - let principal = contentWindow.document.nodePrincipal; - let id = principal.originAttributes.addonId; + let id = ExtensionManagement.getAddonIdForWindow(contentWindow); + + // We don't inject privileged APIs into sub-frames of a UI page. + const { FULL_PRIVILEGES } = ExtensionManagement.API_LEVELS; + if (ExtensionManagement.getAPILevelForWindow(contentWindow, id) !== FULL_PRIVILEGES) { + return; + } + + // We don't inject privileged APIs if the addonId is null + // or doesn't exist. if (!this.extensionMap.has(id)) { return; } @@ -387,10 +391,6 @@ GlobalManager = { return; } - // We don't inject into sub-frames of a UI page. - if (contentWindow != contentWindow.top) { - return; - } let extension = this.extensionMap.get(id); let uri = contentWindow.document.documentURIObject; let incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow); diff --git a/toolkit/components/extensions/ExtensionContent.jsm b/toolkit/components/extensions/ExtensionContent.jsm index bcc2c958e3a3..78a28d515b11 100644 --- a/toolkit/components/extensions/ExtensionContent.jsm +++ b/toolkit/components/extensions/ExtensionContent.jsm @@ -216,7 +216,10 @@ var ExtensionManager; // Scope in which extension content script code can run. It uses // Cu.Sandbox to run the code. There is a separate scope for each // frame. -function ExtensionContext(extensionId, contentWindow) { +function ExtensionContext(extensionId, contentWindow, contextOptions = {}) { + let { isExtensionPage } = contextOptions; + + this.isExtensionPage = isExtensionPage; this.extension = ExtensionManager.get(extensionId); this.extensionId = extensionId; this.contentWindow = contentWindow; @@ -243,12 +246,27 @@ function ExtensionContext(extensionId, contentWindow) { prin = [contentPrincipal, extensionPrincipal]; } - this.sandbox = Cu.Sandbox(prin, { - sandboxPrototype: contentWindow, - wantXrays: true, - isWebExtensionContentScript: true, - wantGlobalProperties: ["XMLHttpRequest"], - }); + if (isExtensionPage) { + if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != extensionId) { + throw new Error("Invalid target window for this extension context"); + } + // This is an iframe with content script API enabled and its principal should be the + // contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled + // because it enables us to create the APIs object in this sandbox object and then copying it + // into the iframe's window, see Bug 1214658 for rationale) + this.sandbox = Cu.Sandbox(contentWindow, { + sandboxPrototype: contentWindow, + wantXrays: false, + isWebExtensionContentScript: true, + }); + } else { + this.sandbox = Cu.Sandbox(prin, { + sandboxPrototype: contentWindow, + wantXrays: true, + isWebExtensionContentScript: true, + wantGlobalProperties: ["XMLHttpRequest"], + }); + } let delegate = { getSender(context, target, sender) { @@ -265,12 +283,19 @@ function ExtensionContext(extensionId, contentWindow) { let filter = {extensionId, frameId}; this.messenger = new Messenger(this, broker, sender, filter, delegate); - let chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "browser"}); + this.chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "browser"}); // Sandboxes don't get Xrays for some weird compatibility // reason. However, we waive here anyway in case that changes. - Cu.waiveXrays(this.sandbox).chrome = Cu.waiveXrays(this.sandbox).browser; - injectAPI(api(this), chromeObj); + Cu.waiveXrays(this.sandbox).chrome = this.chromeObj; + + injectAPI(api(this), this.chromeObj); + + // This is an iframe with content script API enabled. (See Bug 1214658 for rationale) + if (isExtensionPage) { + Cu.waiveXrays(this.contentWindow).chrome = this.chromeObj; + Cu.waiveXrays(this.contentWindow).browser = this.chromeObj; + } } ExtensionContext.prototype = { @@ -294,7 +319,17 @@ ExtensionContext.prototype = { for (let obj of this.onClose) { obj.close(); } + + // Overwrite the content script APIs with an empty object if the APIs objects are still + // defined in the content window (See Bug 1214658 for rationale). + if (this.isExtensionPage && !Cu.isDeadWrapper(this.contentWindow) && + Cu.waiveXrays(this.contentWindow).browser === this.chromeObj) { + Cu.createObjectIn(this.contentWindow, { defineAs: "browser" }); + Cu.createObjectIn(this.contentWindow, { defineAs: "chrome" }); + } + Cu.nukeSandbox(this.sandbox); + this.sandbox = null; }, }; @@ -310,7 +345,10 @@ var DocumentManager = { extensionCount: 0, // Map[windowId -> Map[extensionId -> ExtensionContext]] - windows: new Map(), + contentScriptWindows: new Map(), + + // Map[windowId -> ExtensionContext] + extensionPageWindows: new Map(), init() { Services.obs.addObserver(this, "document-element-inserted", false); @@ -348,23 +386,40 @@ var DocumentManager = { return; } + // Enable the content script APIs should be available in subframes' window + // if it is recognized as a valid addon id (see Bug 1214658 for rationale). + const { CONTENTSCRIPT_PRIVILEGES } = ExtensionManagement.API_LEVELS; + let extensionId = ExtensionManagement.getAddonIdForWindow(window); + + if (ExtensionManagement.getAPILevelForWindow(window, extensionId) == CONTENTSCRIPT_PRIVILEGES && + ExtensionManager.get(extensionId)) { + DocumentManager.getExtensionPageContext(extensionId, window); + } + this.trigger("document_start", window); /* eslint-disable mozilla/balanced-listeners */ window.addEventListener("DOMContentLoaded", this, true); window.addEventListener("load", this, true); /* eslint-enable mozilla/balanced-listeners */ } else if (topic == "inner-window-destroyed") { - let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - if (!this.windows.has(id)) { - return; + let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + + // Close any existent content-script context for the destroyed window. + if (this.contentScriptWindows.has(windowId)) { + let extensions = this.contentScriptWindows.get(windowId); + for (let [, context] of extensions) { + context.close(); + } + + this.contentScriptWindows.delete(windowId); } - let extensions = this.windows.get(id); - for (let [, context] of extensions) { + // Close any existent iframe extension page context for the destroyed window. + if (this.extensionPageWindows.has(windowId)) { + let context = this.extensionWindows.get(windowId); context.close(); + this.extensionPageWindows.delete(windowId); } - - this.windows.delete(id); } }, @@ -389,7 +444,7 @@ var DocumentManager = { executeScript(global, extensionId, script) { let window = global.content; - let context = this.getContext(extensionId, window); + let context = this.getContentScriptContext(extensionId, window); if (!context) { return; } @@ -413,19 +468,33 @@ var DocumentManager = { } }, - getContext(extensionId, window) { + getContentScriptContext(extensionId, window) { let winId = windowId(window); - if (!this.windows.has(winId)) { - this.windows.set(winId, new Map()); + if (!this.contentScriptWindows.has(winId)) { + this.contentScriptWindows.set(winId, new Map()); } - let extensions = this.windows.get(winId); + + let extensions = this.contentScriptWindows.get(winId); if (!extensions.has(extensionId)) { let context = new ExtensionContext(extensionId, window); extensions.set(extensionId, context); } + return extensions.get(extensionId); }, + getExtensionPageContext(extensionId, window) { + let winId = windowId(window); + + let context = this.extensionPageWindows.get(winId); + if (!context) { + let context = new ExtensionContext(extensionId, window, { isExtensionPage: true }); + this.extensionPageWindows.set(winId, context); + } + + return context; + }, + startupExtension(extensionId) { if (this.extensionCount == 0) { this.init(); @@ -440,7 +509,7 @@ var DocumentManager = { for (let [window, state] of this.enumerateWindows(global.docShell)) { for (let script of extension.scripts) { if (script.matches(window)) { - let context = this.getContext(extensionId, window); + let context = this.getContentScriptContext(extensionId, window); context.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state)); } } @@ -449,7 +518,8 @@ var DocumentManager = { }, shutdownExtension(extensionId) { - for (let [, extensions] of this.windows) { + // Clean up content-script contexts on extension shutdown. + for (let [, extensions] of this.contentScriptWindows) { let context = extensions.get(extensionId); if (context) { context.close(); @@ -457,6 +527,14 @@ var DocumentManager = { } } + // Clean up iframe extension page contexts on extension shutdown. + for (let [winId, context] of this.extensionPageWindows) { + if (context.extensionId == extensionId) { + context.close(); + this.extensionPageWindows.delete(winId); + } + } + this.extensionCount--; if (this.extensionCount == 0) { this.uninit(); @@ -468,7 +546,7 @@ var DocumentManager = { for (let [extensionId, extension] of ExtensionManager.extensions) { for (let script of extension.scripts) { if (script.matches(window)) { - let context = this.getContext(extensionId, window); + let context = this.getContentScriptContext(extensionId, window); context.execute(script, scheduled => scheduled == state); } } diff --git a/toolkit/components/extensions/ExtensionManagement.jsm b/toolkit/components/extensions/ExtensionManagement.jsm index 36c29a783050..58f1b9758992 100644 --- a/toolkit/components/extensions/ExtensionManagement.jsm +++ b/toolkit/components/extensions/ExtensionManagement.jsm @@ -202,18 +202,58 @@ var Service = { // This is used to set the addonId on the originAttributes for the // nsIPrincipal attached to the URI. extensionURIToAddonID(uri) { - if (this.extensionURILoadableByAnyone(uri)) { - // We don't want webAccessibleResources to be associated with - // the add-on. That way they don't get any special privileges. - return null; - } - let uuid = uri.host; let extension = this.uuidMap.get(uuid); return extension ? extension.id : undefined; }, }; +// API Levels Helpers + +// Find the add-on associated with this document via the +// principal's originAttributes. This value is computed by +// extensionURIToAddonID, which ensures that we don't inject our +// API into webAccessibleResources or remote web pages. +function getAddonIdForWindow(window) { + let principal = window.document.nodePrincipal; + return principal.originAttributes.addonId; +} + +const API_LEVELS = Object.freeze({ + NO_PRIVILEGES: 0, + CONTENTSCRIPT_PRIVILEGES: 1, + FULL_PRIVILEGES: 2, +}); + +// Finds the API Level ("FULL_PRIVILEGES", "CONTENTSCRIPT_PRIVILEGES", "NO_PRIVILEGES") +// with a given a window object. +function getAPILevelForWindow(window, addonId) { + const { NO_PRIVILEGES, CONTENTSCRIPT_PRIVILEGES, FULL_PRIVILEGES } = API_LEVELS; + + // Non WebExtension URLs and WebExtension URLs from a different extension + // has no access to APIs. + if (!addonId && getAddonIdForWindow(window) != addonId) { + return NO_PRIVILEGES; + } + + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + + // WebExtension URLs loaded into sub-frame UI have "content script API level privileges". + // (see Bug 1214658 for rationale) + if (docShell.sameTypeParent) { + return CONTENTSCRIPT_PRIVILEGES; + } + + // Extension pages running in the content process defaults to "content script API level privileges". + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + return CONTENTSCRIPT_PRIVILEGES; + } + + // WebExtension URLs loaded into top frames UI could have full API level privileges. + return FULL_PRIVILEGES; +} + this.ExtensionManagement = { startupExtension: Service.startupExtension.bind(Service), shutdownExtension: Service.shutdownExtension.bind(Service), @@ -226,4 +266,9 @@ this.ExtensionManagement = { getFrameId: Frames.getId.bind(Frames), getParentFrameId: Frames.getParentId.bind(Frames), + + // exported API Level Helpers + getAddonIdForWindow, + getAPILevelForWindow, + API_LEVELS, };