diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp index 0ee9b72d367b..a8d08f14ed8e 100644 --- a/dom/canvas/CanvasRenderingContext2D.cpp +++ b/dom/canvas/CanvasRenderingContext2D.cpp @@ -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; diff --git a/dom/canvas/CanvasUtils.cpp b/dom/canvas/CanvasUtils.cpp index 36108c3db215..80b2d27b3312 100644 --- a/dom/canvas/CanvasUtils.cpp +++ b/dom/canvas/CanvasUtils.cpp @@ -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(); } diff --git a/dom/html/HTMLCanvasElement.cpp b/dom/html/HTMLCanvasElement.cpp index 077e59e38790..e7894cf6f9f4 100644 --- a/dom/html/HTMLCanvasElement.cpp +++ b/dom/html/HTMLCanvasElement.cpp @@ -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) { diff --git a/dom/html/HTMLCanvasElement.h b/dom/html/HTMLCanvasElement.h index 6cf307e1403e..df878308202e 100644 --- a/dom/html/HTMLCanvasElement.h +++ b/dom/html/HTMLCanvasElement.h @@ -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 mExpandedReader; + + // Determines if the caller should be able to read the content. + bool CallerCanRead(JSContext* aCx); + bool IsPrintCallbackDone(); void HandlePrintCallback(nsPresContext::nsPresContextType aType); diff --git a/toolkit/components/extensions/test/xpcshell/data/pixel_green.gif b/toolkit/components/extensions/test/xpcshell/data/pixel_green.gif new file mode 100644 index 000000000000..baf8166dae98 Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/pixel_green.gif differ diff --git a/toolkit/components/extensions/test/xpcshell/data/pixel_red.gif b/toolkit/components/extensions/test/xpcshell/data/pixel_red.gif new file mode 100644 index 000000000000..48f97f74bd47 Binary files /dev/null and b/toolkit/components/extensions/test/xpcshell/data/pixel_red.gif differ diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_canvas_tainting.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_canvas_tainting.js new file mode 100644 index 000000000000..babd3ab8d002 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_canvas_tainting.js @@ -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(` + + `); +}); + +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(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini b/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini index 952442e3493c..bfee483bda31 100644 --- a/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini +++ b/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini @@ -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