mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-29 07:42:04 +00:00
Bug 1484980 - Add selective canvas tainting for content scripts r=bzbarsky
Reviewers: bzbarsky Bug #: 1484980 Differential Revision: https://phabricator.services.mozilla.com/D6999
This commit is contained in:
parent
f387d9fbfd
commit
7cb46fea7c
@ -5439,13 +5439,7 @@ CanvasRenderingContext2D::GetImageData(JSContext* aCx, double aSx,
|
||||
|
||||
// Check only if we have a canvas element; if we were created with a docshell,
|
||||
// then it's special internal use.
|
||||
if (mCanvasElement && mCanvasElement->IsWriteOnly() &&
|
||||
// We could ask bindings for the caller type, but they already hand us a
|
||||
// JSContext, and we're at least _somewhat_ perf-sensitive (so may not
|
||||
// want to compute the caller type in the common non-write-only case), so
|
||||
// let's just use what we have.
|
||||
!nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::all_urlsPermission))
|
||||
{
|
||||
if (mCanvasElement && !mCanvasElement->CallerCanRead(aCx)) {
|
||||
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
||||
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
||||
return nullptr;
|
||||
|
@ -233,8 +233,9 @@ DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement,
|
||||
return;
|
||||
}
|
||||
|
||||
if (aCanvasElement->IsWriteOnly())
|
||||
if (aCanvasElement->IsWriteOnly() && !aCanvasElement->mExpandedReader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we explicitly set WriteOnly just do it and get out
|
||||
if (forceWriteOnly) {
|
||||
@ -253,6 +254,25 @@ DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement,
|
||||
return;
|
||||
}
|
||||
|
||||
if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
|
||||
// This is a resource from an extension content script principal.
|
||||
|
||||
if (aCanvasElement->mExpandedReader &&
|
||||
aCanvasElement->mExpandedReader->Subsumes(aPrincipal)) {
|
||||
// This canvas already allows reading from this principal.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!aCanvasElement->mExpandedReader) {
|
||||
// Allow future reads from this same princial only.
|
||||
aCanvasElement->SetWriteOnly(aPrincipal);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we got here, this must be the *second* extension tainting
|
||||
// the canvas. Fall through to mark it WriteOnly for everyone.
|
||||
}
|
||||
|
||||
aCanvasElement->SetWriteOnly();
|
||||
}
|
||||
|
||||
|
@ -670,9 +670,8 @@ HTMLCanvasElement::ToDataURL(JSContext* aCx, const nsAString& aType,
|
||||
nsIPrincipal& aSubjectPrincipal,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
// do a trust check if this is a write-only canvas
|
||||
if (mWriteOnly &&
|
||||
!nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::all_urlsPermission)) {
|
||||
// mWriteOnly check is redundant, but optimizes for the common case.
|
||||
if (mWriteOnly && !CallerCanRead(aCx)) {
|
||||
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
||||
return;
|
||||
}
|
||||
@ -881,9 +880,8 @@ HTMLCanvasElement::ToBlob(JSContext* aCx,
|
||||
nsIPrincipal& aSubjectPrincipal,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
// do a trust check if this is a write-only canvas
|
||||
if (mWriteOnly &&
|
||||
!nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::all_urlsPermission)) {
|
||||
// mWriteOnly check is redundant, but optimizes for the common case.
|
||||
if (mWriteOnly && !CallerCanRead(aCx)) {
|
||||
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
||||
return;
|
||||
}
|
||||
@ -1093,9 +1091,36 @@ HTMLCanvasElement::IsWriteOnly()
|
||||
void
|
||||
HTMLCanvasElement::SetWriteOnly()
|
||||
{
|
||||
mExpandedReader = nullptr;
|
||||
mWriteOnly = true;
|
||||
}
|
||||
|
||||
void
|
||||
HTMLCanvasElement::SetWriteOnly(nsIPrincipal* aExpandedReader)
|
||||
{
|
||||
mExpandedReader = aExpandedReader;
|
||||
mWriteOnly = true;
|
||||
}
|
||||
|
||||
bool
|
||||
HTMLCanvasElement::CallerCanRead(JSContext* aCx)
|
||||
{
|
||||
if (!mWriteOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
nsIPrincipal* prin = nsContentUtils::SubjectPrincipal(aCx);
|
||||
|
||||
// If mExpandedReader is set, this canvas was tainted only by
|
||||
// mExpandedReader's resources. So allow reading if the subject
|
||||
// principal subsumes mExpandedReader.
|
||||
if (mExpandedReader && prin->Subsumes(mExpandedReader)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return nsContentUtils::PrincipalHasPermission(prin, nsGkAtoms::all_urlsPermission);
|
||||
}
|
||||
|
||||
void
|
||||
HTMLCanvasElement::InvalidateCanvasContent(const gfx::Rect* damageRect)
|
||||
{
|
||||
|
@ -230,6 +230,12 @@ public:
|
||||
*/
|
||||
void SetWriteOnly();
|
||||
|
||||
/**
|
||||
* Force the canvas to be write-only, except for readers from
|
||||
* a specific extension's content script expanded principal.
|
||||
*/
|
||||
void SetWriteOnly(nsIPrincipal* aExpandedReader);
|
||||
|
||||
/**
|
||||
* Notify that some canvas content has changed and the window may
|
||||
* need to be updated. aDamageRect is in canvas coordinates.
|
||||
@ -395,8 +401,15 @@ public:
|
||||
// We set this when script paints an image from a different origin.
|
||||
// We also transitively set it when script paints a canvas which
|
||||
// is itself write-only.
|
||||
bool mWriteOnly;
|
||||
bool mWriteOnly;
|
||||
|
||||
// When this canvas is (only) tainted by an image from an extension
|
||||
// content script, allow reads from the same extension afterwards.
|
||||
RefPtr<nsIPrincipal> mExpandedReader;
|
||||
|
||||
// Determines if the caller should be able to read the content.
|
||||
bool CallerCanRead(JSContext* aCx);
|
||||
|
||||
bool IsPrintCallbackDone();
|
||||
|
||||
void HandlePrintCallback(nsPresContext::nsPresContextType aType);
|
||||
|
BIN
toolkit/components/extensions/test/xpcshell/data/pixel_green.gif
Normal file
BIN
toolkit/components/extensions/test/xpcshell/data/pixel_green.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 B |
BIN
toolkit/components/extensions/test/xpcshell/data/pixel_red.gif
Normal file
BIN
toolkit/components/extensions/test/xpcshell/data/pixel_red.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 B |
@ -0,0 +1,86 @@
|
||||
"use strict";
|
||||
|
||||
const server = createHttpServer({hosts: ["green.example.com", "red.example.com"]});
|
||||
|
||||
server.registerDirectory("/data/", do_get_file("data"));
|
||||
|
||||
server.registerPathHandler("/pixel.html", (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "text/html", false);
|
||||
response.write(`<!DOCTYPE html>
|
||||
<script>
|
||||
function readByWeb() {
|
||||
let ctx = document.querySelector("canvas").getContext("2d");
|
||||
let {data} = ctx.getImageData(0, 0, 1, 1);
|
||||
return data.slice(0, 3).join();
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
});
|
||||
|
||||
add_task(async function test_contentscript_canvas_tainting() {
|
||||
async function contentScript() {
|
||||
let canvas = document.createElement("canvas");
|
||||
let ctx = canvas.getContext("2d");
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
function draw(url) {
|
||||
return new Promise(resolve => {
|
||||
let img = document.createElement("img");
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, 1, 1);
|
||||
resolve();
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function readByExt() {
|
||||
let {data} = ctx.getImageData(0, 0, 1, 1);
|
||||
return data.slice(0, 3).join();
|
||||
}
|
||||
|
||||
let readByWeb = window.wrappedJSObject.readByWeb;
|
||||
|
||||
// Test reading after drawing an image from the same origin as the web page.
|
||||
await draw("http://green.example.com/data/pixel_green.gif");
|
||||
browser.test.assertEq(readByWeb(), "0,255,0", "Content can read same-origin image");
|
||||
browser.test.assertEq(readByExt(), "0,255,0", "Extension can read same-origin image");
|
||||
|
||||
// Test reading after drawing a blue pixel data URI from extension content script.
|
||||
await draw("data:image/gif;base64,R0lGODlhAQABAIABAAAA/wAAACwAAAAAAQABAAACAkQBADs=");
|
||||
browser.test.assertThrows(readByWeb, /operation is insecure/, "Content can't read extension's image");
|
||||
browser.test.assertEq(readByExt(), "0,0,255", "Extension can read its own image");
|
||||
|
||||
// Test after tainting the canvas with an image from a third party domain.
|
||||
await draw("http://red.example.com/data/pixel_red.gif");
|
||||
browser.test.assertThrows(readByWeb, /operation is insecure/, "Content can't read third party image");
|
||||
browser.test.assertThrows(readByExt, /operation is insecure/, "Extension can't read fully tainted");
|
||||
|
||||
// Test canvas is still fully tainted after drawing extension's data: image again.
|
||||
await draw("data:image/gif;base64,R0lGODlhAQABAIABAAAA/wAAACwAAAAAAQABAAACAkQBADs=");
|
||||
browser.test.assertThrows(readByWeb, /operation is insecure/, "Canvas still fully tainted for content");
|
||||
browser.test.assertThrows(readByExt, /operation is insecure/, "Canvas still fully tainted for extension");
|
||||
|
||||
browser.test.sendMessage("done");
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
content_scripts: [{
|
||||
"matches": ["http://green.example.com/pixel.html"],
|
||||
"js": ["cs.js"],
|
||||
}],
|
||||
},
|
||||
files: {
|
||||
"cs.js": contentScript,
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
let contentPage = await ExtensionTestUtils.loadContentPage("http://green.example.com/pixel.html");
|
||||
await extension.awaitMessage("done");
|
||||
|
||||
await contentPage.close();
|
||||
await extension.unload();
|
||||
});
|
@ -3,6 +3,7 @@ skip-if = os == "android" || (os == "win" && debug) || (os == "linux")
|
||||
[test_ext_i18n_css.js]
|
||||
[test_ext_contentscript.js]
|
||||
[test_ext_contentscript_about_blank_start.js]
|
||||
[test_ext_contentscript_canvas_tainting.js]
|
||||
[test_ext_contentscript_scriptCreated.js]
|
||||
[test_ext_contentscript_triggeringPrincipal.js]
|
||||
skip-if = (os == "android" && debug) || (os == "win" && debug) # Windows: Bug 1438796
|
||||
|
Loading…
Reference in New Issue
Block a user