Bug 1521573 web_accessible incognito support, r=kmag,smaug

Prevent web_accessible_resources resources loading in private contexts when extension does not have permission.

Differential Revision: https://phabricator.services.mozilla.com/D17138

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Shane Caraveo 2019-01-29 15:40:09 +00:00
parent a68eff5db8
commit 3e429f056f
5 changed files with 211 additions and 9 deletions

View File

@ -932,6 +932,19 @@ nsresult nsScriptSecurityManager::CheckLoadURIFlags(
return rv;
}
// Used by ExtensionProtocolHandler to prevent loading extension resources
// in private contexts if the extension does not have permission.
if (aFromPrivateWindow) {
rv = DenyAccessIfURIHasFlags(
aTargetURI, nsIProtocolHandler::URI_DISALLOW_IN_PRIVATE_CONTEXT);
if (NS_FAILED(rv)) {
if (reportErrors) {
ReportError(errorTag, aSourceURI, aTargetURI, aFromPrivateWindow);
}
return rv;
}
}
// Check for chrome target URI
bool hasFlags = false;
rv = NS_URIChainHasFlags(aTargetURI, nsIProtocolHandler::URI_IS_UI_RESOURCE,

View File

@ -330,4 +330,9 @@ interface nsIProtocolHandler : nsISupports
* The URIs for this protocol can be loaded by extensions.
*/
const unsigned long URI_LOADABLE_BY_EXTENSIONS = (1 << 21);
/**
* The URIs for this protocol can not be loaded into private contexts.
*/
const unsigned long URI_DISALLOW_IN_PRIVATE_CONTEXT = (1 << 22);
};

View File

@ -341,20 +341,27 @@ static inline ExtensionPolicyService& EPS() {
nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI,
uint32_t* aFlags) {
// In general a moz-extension URI is only loadable by chrome, but a
// whitelisted subset are web-accessible (and cross-origin fetchable). Check
// that whitelist.
bool loadableByAnyone = false;
uint32_t flags =
URI_STD | URI_IS_LOCAL_RESOURCE | URI_IS_POTENTIALLY_TRUSTWORTHY;
URLInfo url(aURI);
if (auto* policy = EPS().GetByURL(url)) {
loadableByAnyone = policy->IsPathWebAccessible(url.FilePath());
// In general a moz-extension URI is only loadable by chrome, but a
// whitelisted subset are web-accessible (and cross-origin fetchable). Check
// that whitelist.
if (policy->IsPathWebAccessible(url.FilePath())) {
flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE;
} else {
flags |= URI_DANGEROUS_TO_LOAD;
}
// Disallow in private windows if the extension does not have permission.
if (!policy->PrivateBrowsingAllowed()) {
flags |= URI_DISALLOW_IN_PRIVATE_CONTEXT;
}
}
*aFlags =
URI_STD | URI_IS_LOCAL_RESOURCE | URI_IS_POTENTIALLY_TRUSTWORTHY |
(loadableByAnyone ? (URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE)
: URI_DANGEROUS_TO_LOAD);
*aFlags = flags;
return NS_OK;
}

View File

@ -122,6 +122,8 @@ skip-if = os == 'android' || verify # bug 1489771
skip-if = os == 'android'
[test_ext_web_accessible_resources.html]
skip-if = os == 'android' && debug # bug 1397615
[test_ext_web_accessible_incognito.html]
skip-if = os == 'android' # bug 1397615 and bug 1513544
[test_ext_webnavigation.html]
skip-if = os == 'android' && debug # bug 1397615
[test_ext_webnavigation_filters.html]

View File

@ -0,0 +1,175 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test the web_accessible_resources incognito</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/AddTask.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>
<script type="text/javascript">
"use strict";
let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
"ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
async function testImageLoading(src, expectedAction) {
let imageLoadingPromise = new Promise((resolve, reject) => {
let cleanupListeners;
let testImage = new window.Image();
// Set the src via wrappedJSObject so the load is triggered with the
// content page's principal rather than ours.
testImage.wrappedJSObject.setAttribute("src", src);
let loadListener = () => {
cleanupListeners();
resolve(expectedAction === "loaded");
};
let errorListener = (event) => {
cleanupListeners();
resolve(expectedAction === "blocked");
browser.test.log(`+++ image loading ${event.error}`);
};
cleanupListeners = () => {
testImage.removeEventListener("load", loadListener);
testImage.removeEventListener("error", errorListener);
};
testImage.addEventListener("load", loadListener);
testImage.addEventListener("error", errorListener);
});
let success = await imageLoadingPromise;
browser.runtime.sendMessage({name: "image-loading", expectedAction, success});
}
function testScript() {
window.postMessage("test-script-loaded", "*");
}
add_task(async function test_web_accessible_resources_incognito() {
await SpecialPowers.pushPrefEnv({set: [
["extensions.allowPrivateBrowsingByDefault", false],
]});
// This extension will not have access to private browsing so its
// accessible resources should not be able to load in them.
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"web_accessible_resources": [
"image.png",
"test_script.js",
"accessible.html",
],
},
background() {
browser.test.sendMessage("url", browser.extension.getURL(""));
},
files: {
"image.png": IMAGE_ARRAYBUFFER,
"test_script.js": testScript,
"accessible.html": `<html><head>
<meta charset="utf-8">
</head></html>`,
},
});
await extension.startup();
let baseUrl = await extension.awaitMessage("url");
async function content() {
let baseUrl = await browser.runtime.sendMessage({name: "get-url"});
testImageLoading(`${baseUrl}image.png`, "loaded");
let testScriptElement = document.createElement("script");
// Set the src via wrappedJSObject so the load is triggered with the
// content page's principal rather than ours.
testScriptElement.wrappedJSObject.setAttribute("src", `${baseUrl}test_script.js`);
document.head.appendChild(testScriptElement);
let iframe = document.createElement("iframe");
// Set the src via wrappedJSObject so the load is triggered with the
// content page's principal rather than ours.
iframe.wrappedJSObject.setAttribute("src", `${baseUrl}accessible.html`);
document.body.appendChild(iframe);
// eslint-disable-next-line mozilla/balanced-listeners
window.addEventListener("message", event => {
browser.runtime.sendMessage({"name": event.data});
});
}
let pb_extension = ExtensionTestUtils.loadExtension({
incognitoOverride: "spanning",
manifest: {
permissions: ["tabs"],
content_scripts: [{
"matches": ["*://example.com/*/file_sample.html"],
"run_at": "document_end",
"js": ["content_script_helper.js", "content_script.js"],
}],
},
files: {
"content_script_helper.js": `${testImageLoading}`,
"content_script.js": content,
},
background() {
let url = "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html";
let baseUrl;
let window;
browser.runtime.onMessage.addListener(async msg => {
switch (msg.name) {
case "image-loading":
browser.test.assertFalse(msg.success, `Image was ${msg.expectedAction}`);
browser.test.sendMessage(`image-${msg.expectedAction}`);
break;
case "get-url":
return baseUrl;
default:
browser.test.fail(`unexepected message ${msg.name}`);
}
});
browser.test.onMessage.addListener(async (msg, data) => {
if (msg == "start") {
baseUrl = data;
window = await browser.windows.create({url, incognito: true});
}
if (msg == "close") {
browser.windows.remove(window.id);
}
});
},
});
await pb_extension.startup();
consoleMonitor.start([
{message: /may not load or link to.*image.png/},
{message: /may not load or link to.*test_script.js/},
{message: /\<script\> source URI is not allowed in this document/},
{message: /may not load or link to.*accessible.html/},
]);
pb_extension.sendMessage("start", baseUrl);
await pb_extension.awaitMessage("image-loaded");
pb_extension.sendMessage("close");
await extension.unload();
await pb_extension.unload();
await consoleMonitor.finished();
});
</script>
</body>
</html>