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:
Tomislav Jovanovic 2018-09-26 20:29:36 +02:00
parent f387d9fbfd
commit 7cb46fea7c
8 changed files with 154 additions and 15 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

View File

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

View File

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