From 72998c7d50640c35799f90a94089e7c002386e37 Mon Sep 17 00:00:00 2001 From: Kathy Brade Date: Thu, 26 Sep 2013 17:11:19 -0400 Subject: [PATCH] Bug 6253: Add canvas image extraction prompt. (See also Bug #12684, Make "Not now" default for HTML5 canvas permission dialogue, patched by Isis Lovecruft.) This implements a `PopupNotification` [0] which notifies users that a website has attempted to access an HTML5 canvas. The default ordering for buttons is: Not Now Never for this site (recommended) Allow in the future * FIXES #12684 [1] by making "Not Now" the default in the HTML5 canvas fingerprinting permissions dialogue. * Palette icons included in HTML5 canvas permissions PopupNotification UI. The image is freely licensed and obtainable from: https://openclipart.org/image/300px/svg_to_png/21620/ben_palette.png * Includes a CSS whitespace hack from Pearl Crescent to the `CanvasPermissionPromptHelper_init()` function in `browser/base/content/browser.js` for causing the newlines in the `canvas.siteprompt` string (in torbutton.git, in `chrome/locale/en/torbutton.properties`) to render correctly in PopupNotification XUL elements. [2] NOTE: Applying this patch requires an additional patch to TorButton, to store the additional UI strings before localisation. [3] [0]: https://mxr.mozilla.org/mozilla-esr24/source/toolkit/modules/PopupNotifications.jsm [1]: https://bugs.torproject.org/12684 [2]: https://trac.torproject.org/projects/tor/ticket/12684#comment:21 [3]: https://github.com/isislovecruft/torbutton/commit/368e74d62df349b27cf578525c3fa15da19ccdc2 Also includes: Bug 13021: Prompt before allowing Canvas isPointIn*() calls. Display our data extraction prompt and implement site-specific preferences for access to the isPointInPath() and isPointInStroke() methods. Bug 13439: No canvas prompt for content-callers. Both the Inspector and PDF.js raise canvas prompts although they are no danger as they are delivered with the browser itself and are no untrusted content. This patch exempts both of them from canvas prompts, too. If calling `DescribeScriptedCaller` fails neither `scriptFile` nor `scriptLine` are logged. Bug 15640: Place Canvas MediaStream behind site permission prompt. Bug 17446: Ensure that third parties are never able to extract canvas image data, even if the same domain has been given permission previously as a first party. Bug 23393: When processing the ShowCanvasPermissionPrompt message in the main (parent) process, avoid returning an error from the IPC handler if the browser element cannot be obtained. Prior to this change, canvas prompt requests that arrived as a tab was closing would generate an error, which in turn caused the main process to kill the content process. Also, refactor slightly to clarify logic of IsImageExtractionAllowed. --- browser/base/content/browser.js | 129 +++++++++++++++++ browser/base/content/browser.xul | 2 + .../en-US/chrome/browser/browser.properties | 9 +- browser/themes/linux/browser.css | 8 ++ browser/themes/linux/canvas-popup.svg | 19 +++ browser/themes/linux/jar.mn | 1 + browser/themes/osx/browser.css | 8 ++ browser/themes/osx/canvas-popup.svg | 19 +++ browser/themes/osx/jar.mn | 1 + browser/themes/windows/browser.css | 9 ++ browser/themes/windows/canvas-popup.svg | 19 +++ browser/themes/windows/jar.mn | 1 + dom/base/ImageEncoder.cpp | 26 +++- dom/base/ImageEncoder.h | 4 + dom/bindings/Bindings.conf | 2 +- dom/canvas/CanvasRenderingContext2D.cpp | 56 +++++++- dom/canvas/CanvasRenderingContext2D.h | 8 +- dom/canvas/CanvasRenderingContextHelper.cpp | 5 +- dom/canvas/CanvasRenderingContextHelper.h | 4 +- dom/canvas/CanvasUtils.cpp | 133 ++++++++++++++++++ dom/canvas/CanvasUtils.h | 2 + dom/canvas/OffscreenCanvas.cpp | 6 +- dom/html/HTMLCanvasElement.cpp | 62 ++++++-- dom/html/HTMLCanvasElement.h | 8 +- dom/ipc/PBrowser.ipdl | 8 ++ dom/ipc/TabParent.cpp | 17 +++ dom/ipc/TabParent.h | 1 + dom/media/imagecapture/CaptureTask.cpp | 1 + 28 files changed, 533 insertions(+), 35 deletions(-) create mode 100644 browser/themes/linux/canvas-popup.svg create mode 100644 browser/themes/osx/canvas-popup.svg create mode 100644 browser/themes/windows/canvas-popup.svg diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 47b7f6d226363..f0ed289720ade 100755 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1222,6 +1222,7 @@ var gBrowserInit = { BrowserOffline.init(); IndexedDBPromptHelper.init(); + CanvasPermissionPromptHelper.init(); if (AppConstants.E10S_TESTING_ONLY) gRemoteTabsUI.init(); @@ -1570,6 +1571,7 @@ var gBrowserInit = { BrowserOffline.uninit(); IndexedDBPromptHelper.uninit(); + CanvasPermissionPromptHelper.uninit(); LightweightThemeListener.uninit(); PanelUI.uninit(); AutoShowBookmarksToolbar.uninit(); @@ -6261,6 +6263,133 @@ var IndexedDBPromptHelper = { } }; +var CanvasPermissionPromptHelper = { + _permissionsPrompt: "canvas-permissions-prompt", + _notificationIcon: "canvas-notification-icon", + + init: + function CanvasPermissionPromptHelper_init() { + if (document.styleSheets && (document.styleSheets.length > 0)) try { + let ruleText = "panel[popupid=canvas-permissions-prompt] description { white-space: pre-wrap; }"; + let sheet = document.styleSheets[0]; + sheet.insertRule(ruleText, sheet.cssRules.length); + } catch (e) {}; + + Services.obs.addObserver(this, this._permissionsPrompt, false); + }, + + uninit: + function CanvasPermissionPromptHelper_uninit() { + Services.obs.removeObserver(this, this._permissionsPrompt, false); + }, + + // aSubject is an nsIBrowser (e10s) or an nsIDOMWindow (non-e10s). + // aData is an URL string. + observe: + function CanvasPermissionPromptHelper_observe(aSubject, aTopic, aData) { + if (aTopic != this._permissionsPrompt) { + throw new Error("Unexpected topic"); + } + + let browser; + try { + browser = aSubject.QueryInterface(Ci.nsIBrowser); + } catch (e) {} + + if (!browser) { + try { + let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow); + browser = gBrowser.getBrowserForContentWindow(contentWindow); + } catch (e) {} + + if (!browser) { + throw new Error("No browser"); + } + } + + if (!aData) { + throw new Error("Missing URL"); + } + + var uri = makeURI(aData); + if (gBrowser.selectedBrowser !== browser) { + // Must belong to some other window. + return; + } + + // If canvas prompt is already displayed, just return. This is OK (and + // more efficient) since this permission is associated with the top + // browser's URL. + if (PopupNotifications.getNotification(aTopic, browser)) + return; + + var bundleSvc = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + var torBtnBundle; + try { + torBtnBundle = bundleSvc.createBundle( + "chrome://torbutton/locale/torbutton.properties"); + } catch (e) {} + + var message = getLocalizedString("canvas.siteprompt", [ uri.asciiHost ]); + + var mainAction = { + label: getLocalizedString("canvas.notNow"), + accessKey: getLocalizedString("canvas.notNowAccessKey"), + callback: function() { + return null; + } + }; + + var secondaryActions = [ + { + label: getLocalizedString("canvas.never"), + accessKey: getLocalizedString("canvas.neverAccessKey"), + callback: function() { + setCanvasPermission(uri, Ci.nsIPermissionManager.DENY_ACTION); + } + }, + { + label: getLocalizedString("canvas.allow"), + accessKey: getLocalizedString("canvas.allowAccessKey"), + callback: function() { + setCanvasPermission(uri, Ci.nsIPermissionManager.ALLOW_ACTION); + } + } + ]; + + // Since we have a process in place to perform localization for the + // Torbutton extension, get our strings from the extension if possible. + function getLocalizedString(aID, aParams) { + var s; + if (torBtnBundle) try { + if (aParams) + s = torBtnBundle.formatStringFromName(aID, aParams, aParams.length); + else + s = torBtnBundle.GetStringFromName(aID); + } catch (e) {} + + if (!s) { + if (aParams) + s = gNavigatorBundle.getFormattedString(aID, aParams); + else + s = gNavigatorBundle.getString(aID); + } + + return s; + } + + function setCanvasPermission(aURI, aPerm) { + Services.perms.add(aURI, "canvas/extractData", aPerm, + Ci.nsIPermissionManager.EXPIRE_NEVER); + } + + notification = PopupNotifications.show(browser, aTopic, message, + this._notificationIcon, mainAction, + secondaryActions, null); + } +}; + function CanCloseWindow() { // Avoid redundant calls to canClose from showing multiple diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 2c74aecdf607d..9b7ef6195987c 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -726,6 +726,8 @@ tooltiptext="&urlbar.geolocationNotificationAnchor.tooltip;"/> + .notification-inner > .messageCloseButton:not(:h .webextension-popup-browser { border-radius: inherit; } + +.popup-notification-icon[popupid="canvas-permissions-prompt"] { + list-style-image: url(chrome://browser/skin/canvas-popup.svg); +} + +#canvas-notification-icon { + list-style-image: url(chrome://browser/skin/canvas-popup.svg); +} diff --git a/browser/themes/linux/canvas-popup.svg b/browser/themes/linux/canvas-popup.svg new file mode 100644 index 0000000000000..f99eac3ef7cd3 --- /dev/null +++ b/browser/themes/linux/canvas-popup.svg @@ -0,0 +1,19 @@ + + + + image/svg+xmlOpen Clip Art Librarypalette2009-02-17T21:15:25http://openclipart.org/detail/21620/palette-by-benbenclip artclipartcolorcoloriconiconimagemediapaintpaintpalettepalettepngpublic domainsvg + + Layer 1 + + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn index e09029438bcdb..f04984868c083 100644 --- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -132,6 +132,7 @@ browser.jar: #ifdef E10S_TESTING_ONLY skin/classic/browser/e10s-64@2x.png (../shared/e10s-64@2x.png) #endif + skin/classic/browser/canvas-popup.svg [extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: % override chrome://browser/skin/feeds/audioFeedIcon.png chrome://browser/skin/feeds/feedIcon.png diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index e8ac9163e29a7..516ab31222067 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -3404,3 +3404,11 @@ menulist.translate-infobar-element > .menulist-dropmarker { .webextension-popup-browser { border-radius: inherit; } + +#canvas-notification-icon { + list-style-image: url(chrome://browser/skin/canvas-popup.svg); +} + +.popup-notification-icon[popupid="canvas-permissions-prompt"] { + list-style-image: url(chrome://browser/skin/canvas-popup.svg); +} diff --git a/browser/themes/osx/canvas-popup.svg b/browser/themes/osx/canvas-popup.svg new file mode 100644 index 0000000000000..f99eac3ef7cd3 --- /dev/null +++ b/browser/themes/osx/canvas-popup.svg @@ -0,0 +1,19 @@ + + + + image/svg+xmlOpen Clip Art Librarypalette2009-02-17T21:15:25http://openclipart.org/detail/21620/palette-by-benbenclip artclipartcolorcoloriconiconimagemediapaintpaintpalettepalettepngpublic domainsvg + + Layer 1 + + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index fd9b6127d8fae..d4b77f4f87abd 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -212,6 +212,7 @@ browser.jar: #ifdef E10S_TESTING_ONLY skin/classic/browser/e10s-64@2x.png (../shared/e10s-64@2x.png) #endif + skin/classic/browser/canvas-popup.svg [extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: % override chrome://browser/skin/feeds/audioFeedIcon.png chrome://browser/skin/feeds/feedIcon.png diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index 2de5a65455bf5..3a21b9a49e81e 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -2709,3 +2709,12 @@ notification.pluginVulnerable > .notification-inner > .messageCloseButton { padding-top: .9167em; padding-bottom: .9167em; } + +.popup-notification-icon[popupid="canvas-permissions-prompt"] { + list-style-image: url(chrome://browser/skin/canvas-popup.svg); +} + +#canvas-notification-icon { + list-style-image: url(chrome://browser/skin/canvas-popup.svg); +} + diff --git a/browser/themes/windows/canvas-popup.svg b/browser/themes/windows/canvas-popup.svg new file mode 100644 index 0000000000000..f99eac3ef7cd3 --- /dev/null +++ b/browser/themes/windows/canvas-popup.svg @@ -0,0 +1,19 @@ + + + + image/svg+xmlOpen Clip Art Librarypalette2009-02-17T21:15:25http://openclipart.org/detail/21620/palette-by-benbenclip artclipartcolorcoloriconiconimagemediapaintpaintpalettepalettepngpublic domainsvg + + Layer 1 + + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn index 89c589abad452..276906765239a 100644 --- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -196,6 +196,7 @@ browser.jar: #ifdef E10S_TESTING_ONLY skin/classic/browser/e10s-64@2x.png (../shared/e10s-64@2x.png) #endif + skin/classic/browser/canvas-popup.svg [extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: % override chrome://browser/skin/page-livemarks.png chrome://browser/skin/feeds/feedIcon16.png diff --git a/dom/base/ImageEncoder.cpp b/dom/base/ImageEncoder.cpp index a41dee080989a..c5b1f2142fb31 100644 --- a/dom/base/ImageEncoder.cpp +++ b/dom/base/ImageEncoder.cpp @@ -151,6 +151,7 @@ public: EncodingCompleteEvent* aEncodingCompleteEvent, int32_t aFormat, const nsIntSize aSize, + bool aUsePlaceholder, bool aUsingCustomOptions) : mType(aType) , mOptions(aOptions) @@ -160,6 +161,7 @@ public: , mEncodingCompleteEvent(aEncodingCompleteEvent) , mFormat(aFormat) , mSize(aSize) + , mUsePlaceholder(aUsePlaceholder) , mUsingCustomOptions(aUsingCustomOptions) {} @@ -171,6 +173,7 @@ public: mImageBuffer.get(), mFormat, mSize, + mUsePlaceholder, mImage, nullptr, nullptr, @@ -185,6 +188,7 @@ public: mImageBuffer.get(), mFormat, mSize, + mUsePlaceholder, mImage, nullptr, nullptr, @@ -234,6 +238,7 @@ private: RefPtr mEncodingCompleteEvent; int32_t mFormat; const nsIntSize mSize; + bool mUsePlaceholder; bool mUsingCustomOptions; }; @@ -246,6 +251,7 @@ nsresult ImageEncoder::ExtractData(nsAString& aType, const nsAString& aOptions, const nsIntSize aSize, + bool aUsePlaceholder, nsICanvasRenderingContextInternal* aContext, layers::AsyncCanvasRenderer* aRenderer, nsIInputStream** aStream) @@ -255,7 +261,8 @@ ImageEncoder::ExtractData(nsAString& aType, return NS_IMAGELIB_ERROR_NO_ENCODER; } - return ExtractDataInternal(aType, aOptions, nullptr, 0, aSize, nullptr, + return ExtractDataInternal(aType, aOptions, nullptr, 0, aSize, + aUsePlaceholder, nullptr, aContext, aRenderer, aStream, encoder); } @@ -265,6 +272,7 @@ ImageEncoder::ExtractDataFromLayersImageAsync(nsAString& aType, const nsAString& aOptions, bool aUsingCustomOptions, layers::Image* aImage, + bool aUsePlaceholder, EncodeCompleteCallback* aEncodeCallback) { nsCOMPtr encoder = ImageEncoder::GetImageEncoder(aType); @@ -289,6 +297,7 @@ ImageEncoder::ExtractDataFromLayersImageAsync(nsAString& aType, completeEvent, imgIEncoder::INPUT_FORMAT_HOSTARGB, size, + aUsePlaceholder, aUsingCustomOptions); return sThreadPool->Dispatch(event, NS_DISPATCH_NORMAL); } @@ -301,6 +310,7 @@ ImageEncoder::ExtractDataAsync(nsAString& aType, UniquePtr aImageBuffer, int32_t aFormat, const nsIntSize aSize, + bool aUsePlaceholder, EncodeCompleteCallback* aEncodeCallback) { nsCOMPtr encoder = ImageEncoder::GetImageEncoder(aType); @@ -324,6 +334,7 @@ ImageEncoder::ExtractDataAsync(nsAString& aType, completeEvent, aFormat, aSize, + aUsePlaceholder, aUsingCustomOptions); return sThreadPool->Dispatch(event, NS_DISPATCH_NORMAL); } @@ -354,6 +365,7 @@ ImageEncoder::ExtractDataInternal(const nsAString& aType, uint8_t* aImageBuffer, int32_t aFormat, const nsIntSize aSize, + bool aUsePlaceholder, layers::Image* aImage, nsICanvasRenderingContextInternal* aContext, layers::AsyncCanvasRenderer* aRenderer, @@ -368,7 +380,7 @@ ImageEncoder::ExtractDataInternal(const nsAString& aType, // get image bytes nsresult rv; - if (aImageBuffer) { + if (aImageBuffer && !aUsePlaceholder) { if (BufferSizeFromDimensions(aSize.width, aSize.height, 4) == 0) { return NS_ERROR_INVALID_ARG; } @@ -381,17 +393,17 @@ ImageEncoder::ExtractDataInternal(const nsAString& aType, aEncoder, nsPromiseFlatString(aOptions).get(), getter_AddRefs(imgStream)); - } else if (aContext) { + } else if (aContext && !aUsePlaceholder) { NS_ConvertUTF16toUTF8 encoderType(aType); rv = aContext->GetInputStream(encoderType.get(), nsPromiseFlatString(aOptions).get(), getter_AddRefs(imgStream)); - } else if (aRenderer) { + } else if (aRenderer && !aUsePlaceholder) { NS_ConvertUTF16toUTF8 encoderType(aType); rv = aRenderer->GetInputStream(encoderType.get(), nsPromiseFlatString(aOptions).get(), getter_AddRefs(imgStream)); - } else if (aImage) { + } else if (aImage && !aUsePlaceholder) { // It is safe to convert PlanarYCbCr format from YUV to RGB off-main-thread. // Other image formats could have problem to convert format off-main-thread. // So here it uses a help function GetBRGADataSourceSurfaceSync() to convert @@ -467,6 +479,10 @@ ImageEncoder::ExtractDataInternal(const nsAString& aType, if (!emptyCanvas->Map(DataSourceSurface::MapType::WRITE, &map)) { return NS_ERROR_INVALID_ARG; } + if (aUsePlaceholder) { + // If placeholder data was requested, return all-white, opaque image data. + memset(map.mData, 0xFF, 4 * aSize.width * aSize.height); + } rv = aEncoder->InitFromData(map.mData, aSize.width * aSize.height * 4, aSize.width, diff --git a/dom/base/ImageEncoder.h b/dom/base/ImageEncoder.h index fd30a94d473fc..01eaa1b28f363 100644 --- a/dom/base/ImageEncoder.h +++ b/dom/base/ImageEncoder.h @@ -42,6 +42,7 @@ public: static nsresult ExtractData(nsAString& aType, const nsAString& aOptions, const nsIntSize aSize, + bool aUsePlaceholder, nsICanvasRenderingContextInternal* aContext, layers::AsyncCanvasRenderer* aRenderer, nsIInputStream** aStream); @@ -63,6 +64,7 @@ public: UniquePtr aImageBuffer, int32_t aFormat, const nsIntSize aSize, + bool aUsePlaceholder, EncodeCompleteCallback* aEncodeCallback); // Extract an Image asynchronously. Its function is same as ExtractDataAsync @@ -74,6 +76,7 @@ public: const nsAString& aOptions, bool aUsingCustomOptions, layers::Image* aImage, + bool aUsePlaceholder, EncodeCompleteCallback* aEncodeCallback); // Gives you a stream containing the image represented by aImageBuffer. @@ -95,6 +98,7 @@ private: uint8_t* aImageBuffer, int32_t aFormat, const nsIntSize aSize, + bool aUsePlaceholder, layers::Image* aImage, nsICanvasRenderingContextInternal* aContext, layers::AsyncCanvasRenderer* aRenderer, diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index aa7f26ad61dcf..76f9508703154 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -127,7 +127,7 @@ DOMInterfaces = { 'CanvasRenderingContext2D': { 'implicitJSContext': [ - 'createImageData', 'getImageData' + 'createImageData', 'getImageData', 'isPointInPath', 'isPointInStroke' ], 'binaryNames': { 'mozImageSmoothingEnabled': 'imageSmoothingEnabled' diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp index d19c03c1e41f9..9064cacec2962 100644 --- a/dom/canvas/CanvasRenderingContext2D.cpp +++ b/dom/canvas/CanvasRenderingContext2D.cpp @@ -4679,12 +4679,19 @@ CanvasRenderingContext2D::LineDashOffset() const { } bool -CanvasRenderingContext2D::IsPointInPath(double aX, double aY, const CanvasWindingRule& aWinding) +CanvasRenderingContext2D::IsPointInPath(JSContext* aCx, double aX, double aY, const CanvasWindingRule& aWinding) { if (!FloatValidate(aX, aY)) { return false; } + // Check for site-specific permission and return false if no permission. + if (mCanvasElement) { + nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); + if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx)) + return false; + } + EnsureUserSpacePath(aWinding); if (!mPath) { return false; @@ -4697,12 +4704,19 @@ CanvasRenderingContext2D::IsPointInPath(double aX, double aY, const CanvasWindin return mPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform()); } -bool CanvasRenderingContext2D::IsPointInPath(const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding) +bool CanvasRenderingContext2D::IsPointInPath(JSContext* aCx, const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding) { if (!FloatValidate(aX, aY)) { return false; } + // Check for site-specific permission and return false if no permission. + if (mCanvasElement) { + nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); + if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx)) + return false; + } + EnsureTarget(); if (!IsTargetValid()) { return false; @@ -4714,12 +4728,19 @@ bool CanvasRenderingContext2D::IsPointInPath(const CanvasPath& aPath, double aX, } bool -CanvasRenderingContext2D::IsPointInStroke(double aX, double aY) +CanvasRenderingContext2D::IsPointInStroke(JSContext* aCx, double aX, double aY) { if (!FloatValidate(aX, aY)) { return false; } + // Check for site-specific permission and return false if no permission. + if (mCanvasElement) { + nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); + if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx)) + return false; + } + EnsureUserSpacePath(); if (!mPath) { return false; @@ -4741,12 +4762,19 @@ CanvasRenderingContext2D::IsPointInStroke(double aX, double aY) return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mTarget->GetTransform()); } -bool CanvasRenderingContext2D::IsPointInStroke(const CanvasPath& aPath, double aX, double aY) +bool CanvasRenderingContext2D::IsPointInStroke(JSContext* aCx, const CanvasPath& aPath, double aX, double aY) { if (!FloatValidate(aX, aY)) { return false; } + // Check for site-specific permission and return false if no permission. + if (mCanvasElement) { + nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); + if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx)) + return false; + } + EnsureTarget(); if (!IsTargetValid()) { return false; @@ -5780,6 +5808,26 @@ CanvasRenderingContext2D::GetImageDataArray(JSContext* aCx, srcStride = aWidth * 4; } + // Check for site-specific permission and return all-white, opaque pixel + // data if no permission. This check is not needed if the canvas was + // created with a docshell (that is only done for special internal uses). + bool usePlaceholder = false; + if (mCanvasElement) { + nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); + usePlaceholder = !ownerDoc || + !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx); + } + + if (usePlaceholder) { + if (readback) { + readback->Unmap(); + } + + memset(data, 0xFF, len.value()); + *aRetval = darray; + return NS_OK; + } + uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4; if (mOpaque) { diff --git a/dom/canvas/CanvasRenderingContext2D.h b/dom/canvas/CanvasRenderingContext2D.h index c3ee3bdcbc402..dedec39e71884 100644 --- a/dom/canvas/CanvasRenderingContext2D.h +++ b/dom/canvas/CanvasRenderingContext2D.h @@ -195,10 +195,10 @@ public: bool DrawCustomFocusRing(mozilla::dom::Element& aElement); void Clip(const CanvasWindingRule& aWinding); void Clip(const CanvasPath& aPath, const CanvasWindingRule& aWinding); - bool IsPointInPath(double aX, double aY, const CanvasWindingRule& aWinding); - bool IsPointInPath(const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding); - bool IsPointInStroke(double aX, double aY); - bool IsPointInStroke(const CanvasPath& aPath, double aX, double aY); + bool IsPointInPath(JSContext* aCx, double aX, double aY, const CanvasWindingRule& aWinding); + bool IsPointInPath(JSContext* aCx, const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding); + bool IsPointInStroke(JSContext* aCx, double aX, double aY); + bool IsPointInStroke(JSContext* aCx, const CanvasPath& aPath, double aX, double aY); void FillText(const nsAString& aText, double aX, double aY, const Optional& aMaxWidth, mozilla::ErrorResult& aError); diff --git a/dom/canvas/CanvasRenderingContextHelper.cpp b/dom/canvas/CanvasRenderingContextHelper.cpp index 3000e59bdfd94..f4a87b6566c01 100644 --- a/dom/canvas/CanvasRenderingContextHelper.cpp +++ b/dom/canvas/CanvasRenderingContextHelper.cpp @@ -25,6 +25,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx, BlobCallback& aCallback, const nsAString& aType, JS::Handle aParams, + bool aUsePlaceholder, ErrorResult& aRv) { // Encoder callback when encoding is complete. @@ -68,7 +69,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx, RefPtr callback = new EncodeCallback(aGlobal, &aCallback); - ToBlob(aCx, aGlobal, callback, aType, aParams, aRv); + ToBlob(aCx, aGlobal, callback, aType, aParams, aUsePlaceholder, aRv); } void @@ -77,6 +78,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx, EncodeCompleteCallback* aCallback, const nsAString& aType, JS::Handle aParams, + bool aUsePlaceholder, ErrorResult& aRv) { nsAutoString type; @@ -117,6 +119,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx, Move(imageBuffer), format, GetWidthHeight(), + aUsePlaceholder, callback); } diff --git a/dom/canvas/CanvasRenderingContextHelper.h b/dom/canvas/CanvasRenderingContextHelper.h index d28864f02971d..d05ae98cb3627 100644 --- a/dom/canvas/CanvasRenderingContextHelper.h +++ b/dom/canvas/CanvasRenderingContextHelper.h @@ -58,11 +58,11 @@ protected: void ToBlob(JSContext* aCx, nsIGlobalObject* global, BlobCallback& aCallback, const nsAString& aType, JS::Handle aParams, - ErrorResult& aRv); + bool usePlaceholder, ErrorResult& aRv); void ToBlob(JSContext* aCx, nsIGlobalObject* aGlobal, EncodeCompleteCallback* aCallback, const nsAString& aType, JS::Handle aParams, - ErrorResult& aRv); + bool usePlaceholder, ErrorResult& aRv); virtual already_AddRefed CreateContext(CanvasContextType aContextType); diff --git a/dom/canvas/CanvasUtils.cpp b/dom/canvas/CanvasUtils.cpp index c7cfed83f65d4..dceccfd25c763 100644 --- a/dom/canvas/CanvasUtils.cpp +++ b/dom/canvas/CanvasUtils.cpp @@ -15,6 +15,7 @@ #include "nsICanvasRenderingContextInternal.h" #include "nsIHTMLCollection.h" #include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/TabChild.h" #include "nsIPrincipal.h" #include "nsGfxCIID.h" @@ -27,9 +28,141 @@ using namespace mozilla::gfx; +#include "nsIScriptObjectPrincipal.h" +#include "nsIPermissionManager.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozIThirdPartyUtil.h" +#include "nsContentUtils.h" +#include "nsUnicharUtils.h" +#include "nsPrintfCString.h" +#include "nsIConsoleService.h" +#include "jsapi.h" + +#define TOPIC_CANVAS_PERMISSIONS_PROMPT "canvas-permissions-prompt" +#define PERMISSION_CANVAS_EXTRACT_DATA "canvas/extractData" + namespace mozilla { namespace CanvasUtils { +// Check site-specific permission and display prompt if appropriate. +bool IsImageExtractionAllowed(nsIDocument *aDocument, JSContext *aCx) +{ + // Don't proceed if we don't have a document or JavaScript context. + if (!aDocument || !aCx) { + return false; + } + + // Documents with system principal can always extract canvas data. + nsPIDOMWindowOuter *win = aDocument->GetWindow(); + nsCOMPtr sop(do_QueryInterface(win)); + if (sop && nsContentUtils::IsSystemPrincipal(sop->GetPrincipal())) { + return true; + } + + // Always give permission to chrome scripts (e.g. Page Inspector). + if (nsContentUtils::ThreadsafeIsCallerChrome()) { + return true; + } + + // Get the document URI and its spec. + nsIURI *docURI = aDocument->GetDocumentURI(); + nsCString docURISpec; + docURI->GetSpec(docURISpec); + + // Allow local files to extract canvas data. + bool isFileURL; + (void) docURI->SchemeIs("file", &isFileURL); + if (isFileURL) { + return true; + } + + // Get calling script file and line for logging. + JS::AutoFilename scriptFile; + unsigned scriptLine = 0; + bool isScriptKnown = false; + if (JS::DescribeScriptedCaller(aCx, &scriptFile, &scriptLine)) { + isScriptKnown = true; + // Don't show canvas prompt for PDF.js + if (scriptFile.get() && + strcmp(scriptFile.get(), "resource://pdf.js/build/pdf.js") == 0) { + return true; + } + } + + nsIDocument* topLevelDocument = aDocument->GetTopLevelContentDocument(); + nsIURI *topLevelDocURI = topLevelDocument ? topLevelDocument->GetDocumentURI() : nullptr; + nsCString topLevelDocURISpec; + if (topLevelDocURI) { + topLevelDocURI->GetSpec(topLevelDocURISpec); + } + + // Load Third Party Util service. + nsresult rv; + nsCOMPtr thirdPartyUtil = + do_GetService(THIRDPARTYUTIL_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, false); + + // Block all third-party attempts to extract canvas. + bool isThirdParty = true; + rv = thirdPartyUtil->IsThirdPartyURI(topLevelDocURI, docURI, &isThirdParty); + NS_ENSURE_SUCCESS(rv, false); + if (isThirdParty) { + nsAutoCString message; + message.AppendPrintf("Blocked third party %s in page %s from extracting canvas data.", + docURISpec.get(), topLevelDocURISpec.get()); + if (isScriptKnown) { + message.AppendPrintf(" %s:%u.", scriptFile.get(), scriptLine); + } + nsContentUtils::LogMessageToConsole(message.get()); + return false; + } + + // Load Permission Manager service. + nsCOMPtr permissionManager = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID); + NS_ENSURE_SUCCESS(rv, false); + + // Check if the site has permission to extract canvas data. + // Either permit or block extraction if a stored permission setting exists. + uint32_t permission; + rv = permissionManager->TestPermission(topLevelDocURI, + PERMISSION_CANVAS_EXTRACT_DATA, &permission); + NS_ENSURE_SUCCESS(rv, false); + if (permission == nsIPermissionManager::ALLOW_ACTION) { + return true; + } else if (permission == nsIPermissionManager::DENY_ACTION) { + return false; + } + + // At this point, permission is unknown (nsIPermissionManager::UNKNOWN_ACTION). + nsAutoCString message; + message.AppendPrintf("Blocked %s in page %s from extracting canvas data.", + docURISpec.get(), topLevelDocURISpec.get()); + if (isScriptKnown) { + message.AppendPrintf(" %s:%u.", scriptFile.get(), scriptLine); + } + nsContentUtils::LogMessageToConsole(message.get()); + + // Prompt the user (asynchronous). + if (XRE_IsContentProcess()) { + TabChild* tabChild = TabChild::GetFrom(win); + if (tabChild) { + tabChild->SendShowCanvasPermissionPrompt(topLevelDocURISpec); + } + } else { + nsCOMPtr obs = + mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(win, TOPIC_CANVAS_PERMISSIONS_PROMPT, + NS_ConvertUTF8toUTF16(topLevelDocURISpec).get()); + } + } + + // We don't extract the image for now -- user may override at prompt. + return false; +} + bool GetCanvasContextType(const nsAString& str, dom::CanvasContextType* const out_type) { diff --git a/dom/canvas/CanvasUtils.h b/dom/canvas/CanvasUtils.h index a69b8bd729730..c319e1958f11a 100644 --- a/dom/canvas/CanvasUtils.h +++ b/dom/canvas/CanvasUtils.h @@ -46,6 +46,8 @@ void DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement, bool forceWriteOnly, bool CORSUsed); +bool IsImageExtractionAllowed(nsIDocument *aDocument, JSContext *aCx); + // Make a double out of |v|, treating undefined values as 0.0 (for // the sake of sparse arrays). Return true iff coercion // succeeded. diff --git a/dom/canvas/OffscreenCanvas.cpp b/dom/canvas/OffscreenCanvas.cpp index 0d188c24e3392..8a7a494669b56 100644 --- a/dom/canvas/OffscreenCanvas.cpp +++ b/dom/canvas/OffscreenCanvas.cpp @@ -295,8 +295,12 @@ OffscreenCanvas::ToBlob(JSContext* aCx, RefPtr callback = new EncodeCallback(global, promise); + // TODO: Can we obtain the context and document here somehow + // so that we can decide when usePlaceholder should be true/false? + // See https://trac.torproject.org/18599 + // For now, we always return a placeholder. CanvasRenderingContextHelper::ToBlob(aCx, global, - callback, aType, aParams, aRv); + callback, aType, aParams, true /* usePlaceholder */ , aRv); return promise.forget(); } diff --git a/dom/html/HTMLCanvasElement.cpp b/dom/html/HTMLCanvasElement.cpp index 88b41bce0a0c6..360bd7ad4ab2b 100644 --- a/dom/html/HTMLCanvasElement.cpp +++ b/dom/html/HTMLCanvasElement.cpp @@ -41,6 +41,7 @@ #include "nsNetUtil.h" #include "nsRefreshDriver.h" #include "nsStreamUtils.h" +#include "CanvasUtils.h" #include "ActiveLayerTracker.h" #include "VRManagerChild.h" #include "WebGL1Context.h" @@ -60,8 +61,10 @@ class RequestedFrameRefreshObserver : public nsARefreshObserver public: RequestedFrameRefreshObserver(HTMLCanvasElement* const aOwningElement, - nsRefreshDriver* aRefreshDriver) + nsRefreshDriver* aRefreshDriver, + bool aReturnPlaceholderData) : mRegistered(false), + mReturnPlaceholderData(aReturnPlaceholderData), mOwningElement(aOwningElement), mRefreshDriver(aRefreshDriver) { @@ -69,7 +72,8 @@ public: } static already_AddRefed - CopySurface(const RefPtr& aSurface) + CopySurface(const RefPtr& aSurface, + bool aReturnPlaceholderData) { RefPtr data = aSurface->GetDataSurface(); if (!data) { @@ -98,12 +102,23 @@ public: MOZ_ASSERT(data->GetSize() == copy->GetSize()); MOZ_ASSERT(data->GetFormat() == copy->GetFormat()); - memcpy(write.GetData(), read.GetData(), - write.GetStride() * copy->GetSize().height); + if (aReturnPlaceholderData) { + // If returning placeholder data, fill the frame copy with white pixels. + memset(write.GetData(), 0xFF, + write.GetStride() * copy->GetSize().height); + } else { + memcpy(write.GetData(), read.GetData(), + write.GetStride() * copy->GetSize().height); + } return copy.forget(); } + void SetReturnPlaceholderData(bool aReturnPlaceholderData) + { + mReturnPlaceholderData = aReturnPlaceholderData; + } + void WillRefresh(TimeStamp aTime) override { MOZ_ASSERT(NS_IsMainThread()); @@ -131,7 +146,8 @@ public: return; } - RefPtr copy = CopySurface(snapshot); + RefPtr copy = CopySurface(snapshot, + mReturnPlaceholderData); if (!copy) { return; } @@ -183,6 +199,7 @@ private: } bool mRegistered; + bool mReturnPlaceholderData; HTMLCanvasElement* const mOwningElement; RefPtr mRefreshDriver; }; @@ -737,7 +754,13 @@ HTMLCanvasElement::CaptureStream(const Optional& aFrameRate, new CanvasCaptureTrackSource(principal, stream)); stream->AddTrackInternal(track); - rv = RegisterFrameCaptureListener(stream->FrameCaptureListener()); + // Check site-specific permission and display prompt if appropriate. + // If no permission, arrange for the frame capture listener to return + // all-white, opaque image data. + bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(OwnerDoc(), + nsContentUtils::GetCurrentJSContext()); + + rv = RegisterFrameCaptureListener(stream->FrameCaptureListener(), usePlaceholder); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; @@ -747,13 +770,18 @@ HTMLCanvasElement::CaptureStream(const Optional& aFrameRate, } nsresult -HTMLCanvasElement::ExtractData(nsAString& aType, +HTMLCanvasElement::ExtractData(JSContext* aCx, + nsAString& aType, const nsAString& aOptions, nsIInputStream** aStream) { + // Check site-specific permission and display prompt if appropriate. + // If no permission, return all-white, opaque image data. + bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(OwnerDoc(), aCx); return ImageEncoder::ExtractData(aType, aOptions, GetSize(), + usePlaceholder, mCurrentContext, mAsyncCanvasRenderer, aStream); @@ -783,12 +811,12 @@ HTMLCanvasElement::ToDataURLImpl(JSContext* aCx, } nsCOMPtr stream; - rv = ExtractData(type, params, getter_AddRefs(stream)); + rv = ExtractData(aCx, type, params, getter_AddRefs(stream)); // If there are unrecognized custom parse options, we should fall back to // the default values for the encoder without any options at all. if (rv == NS_ERROR_INVALID_ARG && usingCustomParseOptions) { - rv = ExtractData(type, EmptyString(), getter_AddRefs(stream)); + rv = ExtractData(aCx, type, EmptyString(), getter_AddRefs(stream)); } NS_ENSURE_SUCCESS(rv, rv); @@ -820,9 +848,11 @@ HTMLCanvasElement::ToBlob(JSContext* aCx, nsCOMPtr global = OwnerDoc()->GetScopeObject(); MOZ_ASSERT(global); + // Check site-specific permission and display prompt if appropriate. + // If no permission, return all-white, opaque image data. + bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(OwnerDoc(), aCx); CanvasRenderingContextHelper::ToBlob(aCx, global, aCallback, aType, - aParams, aRv); - + aParams, usePlaceholder, aRv); } OffscreenCanvas* @@ -900,7 +930,8 @@ HTMLCanvasElement::MozGetAsBlobImpl(const nsAString& aName, { nsCOMPtr stream; nsAutoString type(aType); - nsresult rv = ExtractData(type, EmptyString(), getter_AddRefs(stream)); + nsresult rv = ExtractData(nsContentUtils::GetCurrentJSContext(), + type, EmptyString(), getter_AddRefs(stream)); NS_ENSURE_SUCCESS(rv, rv); uint64_t imgSize; @@ -1182,7 +1213,8 @@ HTMLCanvasElement::IsContextCleanForFrameCapture() } nsresult -HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener) +HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener, + bool aReturnPlaceholderData) { WeakPtr listener = aListener; @@ -1221,7 +1253,9 @@ HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener) } mRequestedFrameRefreshObserver = - new RequestedFrameRefreshObserver(this, driver); + new RequestedFrameRefreshObserver(this, driver, aReturnPlaceholderData); + } else { + mRequestedFrameRefreshObserver->SetReturnPlaceholderData(aReturnPlaceholderData); } mRequestedFrameListeners.AppendElement(listener); diff --git a/dom/html/HTMLCanvasElement.h b/dom/html/HTMLCanvasElement.h index 81c141d3cc001..e5e72436e8979 100644 --- a/dom/html/HTMLCanvasElement.h +++ b/dom/html/HTMLCanvasElement.h @@ -267,8 +267,11 @@ public: * The registered FrameCaptureListeners are stored as WeakPtrs, thus it's the * caller's responsibility to keep them alive. Once a registered * FrameCaptureListener is destroyed it will be automatically deregistered. + * If aReturnPlaceholderData is true, white data is captured instead of the + * actual canvas contents. */ - nsresult RegisterFrameCaptureListener(FrameCaptureListener* aListener); + nsresult RegisterFrameCaptureListener(FrameCaptureListener* aListener, + bool aReturnPlaceholderData); /* * Returns true when there is at least one registered FrameCaptureListener @@ -364,7 +367,8 @@ protected: virtual already_AddRefed CreateContext(CanvasContextType aContextType) override; - nsresult ExtractData(nsAString& aType, + nsresult ExtractData(JSContext* aCx, + nsAString& aType, const nsAString& aOptions, nsIInputStream** aStream); nsresult ToDataURLImpl(JSContext* aCx, diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl index 9dfccbc5c852a..9187c9ce0067c 100644 --- a/dom/ipc/PBrowser.ipdl +++ b/dom/ipc/PBrowser.ipdl @@ -617,6 +617,14 @@ parent: */ async RequestCrossBrowserNavigation(uint32_t aGlobalIndex); + /** + * This function is used to notify the parent that it should display a + * canvas permission prompt. + * + * @param aFirstPartyURI first party of the tab that is requesting access. + */ + async ShowCanvasPermissionPrompt(nsCString aFirstPartyURI); + child: /** * Notify the remote browser that it has been Show()n on this diff --git a/dom/ipc/TabParent.cpp b/dom/ipc/TabParent.cpp index 8e98de3ce7e6f..acd94e01a8435 100644 --- a/dom/ipc/TabParent.cpp +++ b/dom/ipc/TabParent.cpp @@ -3258,6 +3258,23 @@ TabParent::RecvRequestCrossBrowserNavigation(const uint32_t& aGlobalIndex) return NS_SUCCEEDED(frameLoader->RequestGroupedHistoryNavigation(aGlobalIndex)); } +bool +TabParent::RecvShowCanvasPermissionPrompt(const nsCString& firstPartyURI) +{ + nsCOMPtr browser = do_QueryInterface(mFrameElement); + if (!browser) { + // If the tab is being closed, the browser may not be available. + // In this case we can ignore the request. + return true; + } + + nsCOMPtr os = services::GetObserverService(); + NS_ENSURE_TRUE(os, false); + nsresult rv = os->NotifyObservers(browser, "canvas-permissions-prompt", + NS_ConvertUTF8toUTF16(firstPartyURI).get()); + return NS_SUCCEEDED(rv); +} + NS_IMETHODIMP FakeChannel::OnAuthAvailable(nsISupports *aContext, nsIAuthInformation *aAuthInfo) { diff --git a/dom/ipc/TabParent.h b/dom/ipc/TabParent.h index 09bb999f33e91..da6edc1da4ce6 100644 --- a/dom/ipc/TabParent.h +++ b/dom/ipc/TabParent.h @@ -622,6 +622,7 @@ protected: virtual bool RecvNotifySessionHistoryChange(const uint32_t& aCount) override; virtual bool RecvRequestCrossBrowserNavigation(const uint32_t& aGlobalIndex) override; + virtual bool RecvShowCanvasPermissionPrompt(const nsCString& firstPartyURI) override; ContentCacheInParent mContentCache; diff --git a/dom/media/imagecapture/CaptureTask.cpp b/dom/media/imagecapture/CaptureTask.cpp index 589ba5a42932b..e2da106e63d34 100644 --- a/dom/media/imagecapture/CaptureTask.cpp +++ b/dom/media/imagecapture/CaptureTask.cpp @@ -157,6 +157,7 @@ CaptureTask::SetCurrentFrames(const VideoSegment& aSegment) options, false, image, + false, new EncodeComplete(this)); if (NS_FAILED(rv)) { PostTrackEndEvent(); -- GitLab