Bug 1214658 - Enable content script APIs into sub-frames pointed to a valid add-on url. r=kmag

This commit is contained in:
Luca Greco 2016-01-22 06:02:00 -05:00
Родитель 4763acc777
Коммит d1682407d2
3 изменённых файлов: 165 добавлений и 42 удалений

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

@ -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);

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

@ -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);
}
}

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

@ -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,
};