Back out 3 changesets (bug 1214658) for b2g emulator timeouts in test_ext_contentscript_*

CLOSED TREE

Backed out changeset b76ab3324cd2 (bug 1214658)
Backed out changeset aee8341f15c7 (bug 1214658)
Backed out changeset 743d7567b280 (bug 1214658)
This commit is contained in:
Phil Ringnalda 2016-01-23 13:06:49 -08:00
parent 9423992730
commit 68b7866dcd
8 changed files with 44 additions and 420 deletions

View File

@ -363,16 +363,12 @@ GlobalManager = {
Schemas.inject(chromeObj, schemaWrapper);
};
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.
// 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;
if (!this.extensionMap.has(id)) {
return;
}
@ -391,6 +387,10 @@ 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);

View File

@ -216,10 +216,7 @@ 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, contextOptions = {}) {
let { isExtensionPage } = contextOptions;
this.isExtensionPage = isExtensionPage;
function ExtensionContext(extensionId, contentWindow) {
this.extension = ExtensionManager.get(extensionId);
this.extensionId = extensionId;
this.contentWindow = contentWindow;
@ -246,27 +243,12 @@ function ExtensionContext(extensionId, contentWindow, contextOptions = {}) {
prin = [contentPrincipal, extensionPrincipal];
}
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"],
});
}
this.sandbox = Cu.Sandbox(prin, {
sandboxPrototype: contentWindow,
wantXrays: true,
isWebExtensionContentScript: true,
wantGlobalProperties: ["XMLHttpRequest"],
});
let delegate = {
getSender(context, target, sender) {
@ -283,19 +265,12 @@ function ExtensionContext(extensionId, contentWindow, contextOptions = {}) {
let filter = {extensionId, frameId};
this.messenger = new Messenger(this, broker, sender, filter, delegate);
this.chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "browser"});
let 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 = 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;
}
Cu.waiveXrays(this.sandbox).chrome = Cu.waiveXrays(this.sandbox).browser;
injectAPI(api(this), chromeObj);
}
ExtensionContext.prototype = {
@ -319,17 +294,7 @@ 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;
},
};
@ -345,10 +310,7 @@ var DocumentManager = {
extensionCount: 0,
// Map[windowId -> Map[extensionId -> ExtensionContext]]
contentScriptWindows: new Map(),
// Map[windowId -> ExtensionContext]
extensionPageWindows: new Map(),
windows: new Map(),
init() {
Services.obs.addObserver(this, "document-element-inserted", false);
@ -386,40 +348,23 @@ 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 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 id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
if (!this.windows.has(id)) {
return;
}
// Close any existent iframe extension page context for the destroyed window.
if (this.extensionPageWindows.has(windowId)) {
let context = this.extensionWindows.get(windowId);
let extensions = this.windows.get(id);
for (let [, context] of extensions) {
context.close();
this.extensionPageWindows.delete(windowId);
}
this.windows.delete(id);
}
},
@ -444,7 +389,7 @@ var DocumentManager = {
executeScript(global, extensionId, script) {
let window = global.content;
let context = this.getContentScriptContext(extensionId, window);
let context = this.getContext(extensionId, window);
if (!context) {
return;
}
@ -468,33 +413,19 @@ var DocumentManager = {
}
},
getContentScriptContext(extensionId, window) {
getContext(extensionId, window) {
let winId = windowId(window);
if (!this.contentScriptWindows.has(winId)) {
this.contentScriptWindows.set(winId, new Map());
if (!this.windows.has(winId)) {
this.windows.set(winId, new Map());
}
let extensions = this.contentScriptWindows.get(winId);
let extensions = this.windows.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();
@ -509,7 +440,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.getContentScriptContext(extensionId, window);
let context = this.getContext(extensionId, window);
context.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state));
}
}
@ -518,8 +449,7 @@ var DocumentManager = {
},
shutdownExtension(extensionId) {
// Clean up content-script contexts on extension shutdown.
for (let [, extensions] of this.contentScriptWindows) {
for (let [, extensions] of this.windows) {
let context = extensions.get(extensionId);
if (context) {
context.close();
@ -527,14 +457,6 @@ 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();
@ -546,7 +468,7 @@ var DocumentManager = {
for (let [extensionId, extension] of ExtensionManager.extensions) {
for (let script of extension.scripts) {
if (script.matches(window)) {
let context = this.getContentScriptContext(extensionId, window);
let context = this.getContext(extensionId, window);
context.execute(script, scheduled => scheduled == state);
}
}

View File

@ -202,58 +202,18 @@ 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),
@ -266,9 +226,4 @@ this.ExtensionManagement = {
getFrameId: Frames.getId.bind(Frames),
getParentFrameId: Frames.getParentId.bind(Frames),
// exported API Level Helpers
getAddonIdForWindow,
getAPILevelForWindow,
API_LEVELS,
};

View File

@ -20,15 +20,13 @@ support-files =
file_sample.html
redirection.sjs
file_privilege_escalation.html
file_ext_test_api_injection.js
file_ext_background_api_injection.js
file_permission_xhr.html
[test_ext_simple.html]
[test_ext_geturl.html]
[test_ext_contentscript.html]
skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
[test_ext_contentscript_create_iframe.html]
[test_ext_contentscript_api_injection.html]
[test_ext_i18n_css.html]
[test_ext_generate.html]
[test_ext_localStorage.html]

View File

@ -25,7 +25,7 @@ add_task(function* testBackgroundWindow() {
let awaitConsole = new Promise(resolve => {
let chromeScript = SpecialPowers.loadChromeScript(
SimpleTest.getTestFileURL("file_ext_test_api_injection.js"));
SimpleTest.getTestFileURL("file_ext_background_api_injection.js"));
chromeScript.addMessageListener("console-message", resolve);
});

View File

@ -1,88 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for privilege escalation into iframe with content script APIs</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it -->
<textarea id="test-asset">
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="./content_script_iframe.js">
</script>
</head>
</html>
</textarea>
<script type="text/javascript">
"use strict";
add_task(function* test_contentscript_api_injection() {
function contentScript() {
let iframe = document.createElement("iframe");
iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html"));
document.body.appendChild(iframe);
}
function contentScriptIframe() {
const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
window.location = `${BASE}/file_privilege_escalation.html`;
}
let extensionData = {
manifest: {
content_scripts: [
{
"matches": ["http://mochi.test/*/file_sample.html"],
"js": ["content_script.js"],
"run_at": "document_end",
},
],
"web_accessible_resources": [
"content_script_iframe.html",
],
},
files: {
"content_script.js": "new " + contentScript,
"content_script_iframe.js": "new " + contentScriptIframe,
"content_script_iframe.html": document.querySelector("#test-asset").textContent,
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
let awaitConsole = new Promise(resolve => {
let chromeScript = SpecialPowers.loadChromeScript(
SimpleTest.getTestFileURL("file_ext_test_api_injection.js"));
chromeScript.addMessageListener("console-message", resolve);
});
yield extension.startup();
info("extension loaded");
let win = window.open("file_sample.html");
let message = yield awaitConsole;
ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"),
"Document does not have `browser` APIs.");
win.close();
yield extension.unload();
info("extension unloaded");
});
</script>
</body>
</html>

View File

@ -1,163 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for content script</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it -->
<textarea id="test-asset">
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="content_script_iframe.js"></script>
</head>
</html>
</textarea>
<script type="text/javascript">
"use strict";
add_task(function* test_contentscript_create_iframe() {
function backgroundScript() {
browser.runtime.onMessage.addListener((msg, sender) => {
let { name, availableAPIs, manifest, testGetManifest } = msg;
let hasExtTabsAPI = availableAPIs.indexOf("tabs") > 0;
let hasExtWindowsAPI = availableAPIs.indexOf("windows") > 0;
browser.test.assertFalse(hasExtTabsAPI, "the created iframe should not be able to use privileged APIs (tabs)");
browser.test.assertFalse(hasExtWindowsAPI, "the created iframe should not be able to use privileged APIs (windows)");
let { applications: { gecko: { id: expectedManifestGeckoId } } } = chrome.runtime.getManifest();
let { applications: { gecko: { id: actualManifestGeckoId } } } = manifest;
browser.test.assertEq(actualManifestGeckoId, expectedManifestGeckoId,
"the add-on manifest should be accessible from the created iframe"
);
let { applications: { gecko: { id: testGetManifestGeckoId } } } = testGetManifest;
browser.test.assertEq(testGetManifestGeckoId, expectedManifestGeckoId,
"GET_MANIFEST() returns manifest data before extension unload"
);
browser.test.sendMessage(name);
});
}
function contentScript() {
let iframe = document.createElement("iframe");
iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html"));
document.body.appendChild(iframe);
}
function contentScriptIframe() {
window.GET_MANIFEST = browser.runtime.getManifest.bind(null);
window.testGetManifestException = () => {
try {
window.GET_MANIFEST();
} catch (exception) {
return String(exception);
}
};
let testGetManifest = window.GET_MANIFEST();
let manifest = browser.runtime.getManifest();
let availableAPIs = Object.keys(browser);
browser.runtime.sendMessage({
name: "content-script-iframe-loaded",
availableAPIs,
manifest,
testGetManifest,
});
}
let extensionData = {
manifest: {
content_scripts: [
{
"matches": ["http://mochi.test/*/file_sample.html"],
"js": ["content_script.js"],
"run_at": "document_end",
},
],
web_accessible_resources: [
"content_script_iframe.html",
],
},
background: "(" + backgroundScript + ")()",
files: {
"content_script.js": "new " + contentScript,
"content_script_iframe.html": document.querySelector("#test-asset").textContent,
"content_script_iframe.js": "new " + contentScriptIframe,
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
let contentScriptIframeCreatedPromise = new Promise(resolve => {
extension.onMessage("content-script-iframe-loaded", () => { resolve(); });
});
yield extension.startup();
info("extension loaded");
let win = window.open("file_sample.html");
yield Promise.all([waitForLoad(win), contentScriptIframeCreatedPromise]);
info("content script privileged iframe loaded and executed");
info("testing APIs availability once the extension is unloaded...");
let iframeWindow = SpecialPowers.wrap(win)[0];
ok(iframeWindow, "content script enabled iframe found");
ok(/content_script_iframe\.html$/.test(iframeWindow.location), "the found iframe has the expected URL");
yield extension.unload();
info("extension unloaded");
info("test content script APIs not accessible from the frame once the extension is unloaded");
let ww = SpecialPowers.Cu.waiveXrays(iframeWindow);
let isDeadWrapper = SpecialPowers.Cu.isDeadWrapper(ww.browser);
ok(!isDeadWrapper, "the API object should not be a dead object");
let manifest;
let manifestException;
try {
manifest = ww.browser.runtime.getManifest();
} catch (e) {
manifestException = e;
}
ok(!manifest, "manifest should be undefined");
is(String(manifestException), "TypeError: ww.browser.runtime is undefined",
"expected \"TypeError: ... is undefined\" exception received");
let getManifestException = ww.testGetManifestException();
is(getManifestException, "TypeError: window.GET_MANIFEST is not a function",
"expected \"TypeError: ... is not a function\" exception received");
win.close();
info("done");
});
</script>
</body>
</html>