зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f387d9fbfd
Коммит
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,
|
// Check only if we have a canvas element; if we were created with a docshell,
|
||||||
// then it's special internal use.
|
// then it's special internal use.
|
||||||
if (mCanvasElement && mCanvasElement->IsWriteOnly() &&
|
if (mCanvasElement && !mCanvasElement->CallerCanRead(aCx)) {
|
||||||
// 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))
|
|
||||||
{
|
|
||||||
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
||||||
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
|
@ -233,8 +233,9 @@ DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aCanvasElement->IsWriteOnly())
|
if (aCanvasElement->IsWriteOnly() && !aCanvasElement->mExpandedReader) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If we explicitly set WriteOnly just do it and get out
|
// If we explicitly set WriteOnly just do it and get out
|
||||||
if (forceWriteOnly) {
|
if (forceWriteOnly) {
|
||||||
|
@ -253,6 +254,25 @@ DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement,
|
||||||
return;
|
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();
|
aCanvasElement->SetWriteOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -670,9 +670,8 @@ HTMLCanvasElement::ToDataURL(JSContext* aCx, const nsAString& aType,
|
||||||
nsIPrincipal& aSubjectPrincipal,
|
nsIPrincipal& aSubjectPrincipal,
|
||||||
ErrorResult& aRv)
|
ErrorResult& aRv)
|
||||||
{
|
{
|
||||||
// do a trust check if this is a write-only canvas
|
// mWriteOnly check is redundant, but optimizes for the common case.
|
||||||
if (mWriteOnly &&
|
if (mWriteOnly && !CallerCanRead(aCx)) {
|
||||||
!nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::all_urlsPermission)) {
|
|
||||||
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -881,9 +880,8 @@ HTMLCanvasElement::ToBlob(JSContext* aCx,
|
||||||
nsIPrincipal& aSubjectPrincipal,
|
nsIPrincipal& aSubjectPrincipal,
|
||||||
ErrorResult& aRv)
|
ErrorResult& aRv)
|
||||||
{
|
{
|
||||||
// do a trust check if this is a write-only canvas
|
// mWriteOnly check is redundant, but optimizes for the common case.
|
||||||
if (mWriteOnly &&
|
if (mWriteOnly && !CallerCanRead(aCx)) {
|
||||||
!nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::all_urlsPermission)) {
|
|
||||||
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1093,9 +1091,36 @@ HTMLCanvasElement::IsWriteOnly()
|
||||||
void
|
void
|
||||||
HTMLCanvasElement::SetWriteOnly()
|
HTMLCanvasElement::SetWriteOnly()
|
||||||
{
|
{
|
||||||
|
mExpandedReader = nullptr;
|
||||||
mWriteOnly = true;
|
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
|
void
|
||||||
HTMLCanvasElement::InvalidateCanvasContent(const gfx::Rect* damageRect)
|
HTMLCanvasElement::InvalidateCanvasContent(const gfx::Rect* damageRect)
|
||||||
{
|
{
|
||||||
|
|
|
@ -230,6 +230,12 @@ public:
|
||||||
*/
|
*/
|
||||||
void SetWriteOnly();
|
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
|
* Notify that some canvas content has changed and the window may
|
||||||
* need to be updated. aDamageRect is in canvas coordinates.
|
* 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 set this when script paints an image from a different origin.
|
||||||
// We also transitively set it when script paints a canvas which
|
// We also transitively set it when script paints a canvas which
|
||||||
// is itself write-only.
|
// 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();
|
bool IsPrintCallbackDone();
|
||||||
|
|
||||||
void HandlePrintCallback(nsPresContext::nsPresContextType aType);
|
void HandlePrintCallback(nsPresContext::nsPresContextType aType);
|
||||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 35 B |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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_i18n_css.js]
|
||||||
[test_ext_contentscript.js]
|
[test_ext_contentscript.js]
|
||||||
[test_ext_contentscript_about_blank_start.js]
|
[test_ext_contentscript_about_blank_start.js]
|
||||||
|
[test_ext_contentscript_canvas_tainting.js]
|
||||||
[test_ext_contentscript_scriptCreated.js]
|
[test_ext_contentscript_scriptCreated.js]
|
||||||
[test_ext_contentscript_triggeringPrincipal.js]
|
[test_ext_contentscript_triggeringPrincipal.js]
|
||||||
skip-if = (os == "android" && debug) || (os == "win" && debug) # Windows: Bug 1438796
|
skip-if = (os == "android" && debug) || (os == "win" && debug) # Windows: Bug 1438796
|
||||||
|
|
Загрузка…
Ссылка в новой задаче