Bug 967895 - Ask for placeholder data when image extraction is not allowed. (Tor 6253) draft
authorJonathan Hao <jhao@mozilla.com>
Fri, 12 May 2017 15:39:14 +0800
changeset 593939 c9dc65b30198580384fdcd4251934b9323a4ae99
parent 593938 cc886c2ecffbc68a75cb8d6f49003b2087e8ed3c
child 593940 938122ab3d60071a12385a44ea9630043dd523de
push id63879
push userbmo:jhao@mozilla.com
push dateWed, 14 Jun 2017 09:49:53 +0000
bugs967895, 6253, 12684, 21620
milestone55.0a1
Bug 967895 - Ask for placeholder data when image extraction is not allowed. (Tor 6253) Preventing canvas data extraction is an important part of the anti-fingerprinting work. When the pref privacy.resistFingerprinting is on and content is trying to extract canvas data, we will ask for user permission by a PopupNotification. The three patches of bug 967895 are uplifted from Tor Browser. This patch adds a IsImageExtractionAllowed() helper. When image extraction is not allowed, we asked for a placeholder data. See the original commit message below. From 2c9aa3b1c278653c27a0339db89675ad89933ec9 Mon Sep 17 00:00:00 2001 From: Kathy Brade <brade@pearlcrescent.com> Date: Thu, 26 Sep 2013 17:11:19 -0400 Subject: 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 <description> 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: MozReview-Commit-ID: 2oKYhTeVNdr
dom/bindings/Bindings.conf
dom/canvas/CanvasRenderingContext2D.cpp
dom/canvas/CanvasRenderingContext2D.h
dom/canvas/CanvasRenderingContextHelper.cpp
dom/canvas/CanvasRenderingContextHelper.h
dom/canvas/CanvasUtils.cpp
dom/canvas/CanvasUtils.h
dom/canvas/OffscreenCanvas.cpp
dom/html/HTMLCanvasElement.cpp
dom/html/HTMLCanvasElement.h
dom/ipc/PBrowser.ipdl
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
dom/media/imagecapture/CaptureTask.cpp
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -117,17 +117,17 @@ DOMInterfaces = {
 },
 
 'CacheStorage': {
     'nativeType': 'mozilla::dom::cache::CacheStorage',
 },
 
 'CanvasRenderingContext2D': {
     'implicitJSContext': [
-        'createImageData', 'getImageData'
+        'createImageData', 'getImageData', 'isPointInPath', 'isPointInStroke'
     ],
     'binaryNames': {
         'mozImageSmoothingEnabled': 'imageSmoothingEnabled'
     }
 },
 
 'CaretPosition' : {
     'nativeType': 'nsDOMCaretPosition',
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -4913,57 +4913,71 @@ CanvasRenderingContext2D::SetLineDashOff
 }
 
 double
 CanvasRenderingContext2D::LineDashOffset() const {
   return CurrentState().dashOffset;
 }
 
 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<nsIDocument> ownerDoc = mCanvasElement->OwnerDoc();
+    if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx))
+      return false;
+  }
+
   EnsureUserSpacePath(aWinding);
   if (!mPath) {
     return false;
   }
 
   if (mPathTransformWillUpdate) {
     return mPath->ContainsPoint(Point(aX, aY), mPathToDS);
   }
 
   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;
   }
 
   EnsureTarget();
   if (!IsTargetValid()) {
     return false;
   }
 
   RefPtr<gfx::Path> tempPath = aPath.GetPath(aWinding, mTarget);
 
   return tempPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform());
 }
 
 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<nsIDocument> ownerDoc = mCanvasElement->OwnerDoc();
+    if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx))
+      return false;
+  }
+
   EnsureUserSpacePath();
   if (!mPath) {
     return false;
   }
 
   const ContextState &state = CurrentState();
 
   StrokeOptions strokeOptions(state.lineWidth,
@@ -4975,17 +4989,17 @@ CanvasRenderingContext2D::IsPointInStrok
                               state.dashOffset);
 
   if (mPathTransformWillUpdate) {
     return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mPathToDS);
   }
   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;
   }
 
   EnsureTarget();
   if (!IsTargetValid()) {
     return false;
@@ -5908,16 +5922,37 @@ CanvasRenderingContext2D::GetImageDataAr
   {
     JS::AutoCheckCannotGC nogc;
     bool isShared;
     uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc);
     MOZ_ASSERT(!isShared);        // Should not happen, data was created above
 
     uint32_t srcStride = rawData.mStride;
     uint8_t* src = rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 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<nsIDocument> 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) {
       SwizzleData(src, srcStride, SurfaceFormat::X8R8G8B8_UINT32,
                   dst, aWidth * 4, SurfaceFormat::R8G8B8A8,
                   dstWriteRect.Size());
     } else {
       UnpremultiplyData(src, srcStride, SurfaceFormat::A8R8G8B8_UINT32,
--- a/dom/canvas/CanvasRenderingContext2D.h
+++ b/dom/canvas/CanvasRenderingContext2D.h
@@ -199,20 +199,20 @@ public:
   void Fill(const CanvasWindingRule& aWinding);
   void Fill(const CanvasPath& aPath, const CanvasWindingRule& aWinding);
   void Stroke();
   void Stroke(const CanvasPath& aPath);
   void DrawFocusIfNeeded(mozilla::dom::Element& aElement, ErrorResult& aRv);
   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<double>& aMaxWidth,
                 mozilla::ErrorResult& aError);
   void StrokeText(const nsAString& aText, double aX, double aY,
                   const Optional<double>& aMaxWidth,
                   mozilla::ErrorResult& aError);
   TextMetrics*
     MeasureText(const nsAString& aRawText, mozilla::ErrorResult& aError);
--- a/dom/canvas/CanvasRenderingContextHelper.cpp
+++ b/dom/canvas/CanvasRenderingContextHelper.cpp
@@ -20,16 +20,17 @@ namespace mozilla {
 namespace dom {
 
 void
 CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
                                      nsIGlobalObject* aGlobal,
                                      BlobCallback& aCallback,
                                      const nsAString& aType,
                                      JS::Handle<JS::Value> aParams,
+                                     bool aUsePlaceholder,
                                      ErrorResult& aRv)
 {
   // Encoder callback when encoding is complete.
   class EncodeCallback : public EncodeCompleteCallback
   {
   public:
     EncodeCallback(nsIGlobalObject* aGlobal, BlobCallback* aCallback)
       : mGlobal(aGlobal)
@@ -63,25 +64,26 @@ CanvasRenderingContextHelper::ToBlob(JSC
 
     nsCOMPtr<nsIGlobalObject> mGlobal;
     RefPtr<BlobCallback> mBlobCallback;
   };
 
   RefPtr<EncodeCompleteCallback> callback =
     new EncodeCallback(aGlobal, &aCallback);
 
-  ToBlob(aCx, aGlobal, callback, aType, aParams, aRv);
+  ToBlob(aCx, aGlobal, callback, aType, aParams, aUsePlaceholder, aRv);
 }
 
 void
 CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
                                      nsIGlobalObject* aGlobal,
                                      EncodeCompleteCallback* aCallback,
                                      const nsAString& aType,
                                      JS::Handle<JS::Value> aParams,
+                                     bool aUsePlaceholder,
                                      ErrorResult& aRv)
 {
   nsAutoString type;
   nsContentUtils::ASCIIToLower(aType, type);
 
   nsAutoString params;
   bool usingCustomParseOptions;
   aRv = ParseParams(aCx, type, aParams, params, &usingCustomParseOptions);
@@ -112,16 +114,17 @@ CanvasRenderingContextHelper::ToBlob(JSC
   RefPtr<EncodeCompleteCallback> callback = aCallback;
 
   aRv = ImageEncoder::ExtractDataAsync(type,
                                        params,
                                        usingCustomParseOptions,
                                        Move(imageBuffer),
                                        format,
                                        GetWidthHeight(),
+                                       aUsePlaceholder,
                                        callback);
 }
 
 already_AddRefed<nsICanvasRenderingContextInternal>
 CanvasRenderingContextHelper::CreateContext(CanvasContextType aContextType)
 {
   return CreateContextHelper(aContextType, layers::LayersBackend::LAYERS_NONE);
 }
--- a/dom/canvas/CanvasRenderingContextHelper.h
+++ b/dom/canvas/CanvasRenderingContextHelper.h
@@ -53,21 +53,21 @@ protected:
   virtual nsresult ParseParams(JSContext* aCx,
                                const nsAString& aType,
                                const JS::Value& aEncoderOptions,
                                nsAString& outParams,
                                bool* const outCustomParseOptions);
 
   void ToBlob(JSContext* aCx, nsIGlobalObject* global, BlobCallback& aCallback,
               const nsAString& aType, JS::Handle<JS::Value> aParams,
-              ErrorResult& aRv);
+              bool usePlaceholder, ErrorResult& aRv);
 
   void ToBlob(JSContext* aCx, nsIGlobalObject* aGlobal, EncodeCompleteCallback* aCallback,
               const nsAString& aType, JS::Handle<JS::Value> aParams,
-              ErrorResult& aRv);
+              bool usePlaceholder, ErrorResult& aRv);
 
   virtual already_AddRefed<nsICanvasRenderingContextInternal>
   CreateContext(CanvasContextType aContextType);
 
   already_AddRefed<nsICanvasRenderingContextInternal>
   CreateContextHelper(CanvasContextType aContextType,
                       layers::LayersBackend aCompositorBackend);
 
--- a/dom/canvas/CanvasUtils.cpp
+++ b/dom/canvas/CanvasUtils.cpp
@@ -8,31 +8,169 @@
 
 #include "nsIServiceManager.h"
 
 #include "nsIConsoleService.h"
 #include "nsIDOMCanvasRenderingContext2D.h"
 #include "nsICanvasRenderingContextInternal.h"
 #include "nsIHTMLCollection.h"
 #include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/TabChild.h"
 #include "nsIPrincipal.h"
 
 #include "nsGfxCIID.h"
 
 #include "nsTArray.h"
 
 #include "CanvasUtils.h"
 #include "mozilla/gfx/Matrix.h"
 #include "WebGL2Context.h"
 
 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)
+{
+    // Do the rest of the checks only if privacy.resistFingerprinting is on.
+    if (!nsContentUtils::ShouldResistFingerprinting()) {
+      return true;
+    }
+
+    // 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<nsIScriptObjectPrincipal> 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<mozIThirdPartyUtil> 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<nsIPermissionManager> 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<nsIObserverService> 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)
 {
   if (str.EqualsLiteral("2d")) {
     *out_type = dom::CanvasContextType::Canvas2D;
     return true;
   }
 
--- a/dom/canvas/CanvasUtils.h
+++ b/dom/canvas/CanvasUtils.h
@@ -44,16 +44,18 @@ inline bool CheckSaneSubrectSize(int32_t
 void DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement,
                               nsIPrincipal *aPrincipal,
                               bool forceWriteOnly,
                               bool CORSUsed);
 
 // Check if the context is chrome or has the permission to drawWindow
 bool HasDrawWindowPrivilege(JSContext* aCx, JSObject* aObj);
 
+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.
 bool CoerceDouble(const JS::Value& v, double* d);
 
     /* Float validation stuff */
 #define VALIDATE(_f)  if (!IsFinite(_f)) return false
 
--- a/dom/canvas/OffscreenCanvas.cpp
+++ b/dom/canvas/OffscreenCanvas.cpp
@@ -290,18 +290,23 @@ OffscreenCanvas::ToBlob(JSContext* aCx,
 
     nsCOMPtr<nsIGlobalObject> mGlobal;
     RefPtr<Promise> mPromise;
   };
 
   RefPtr<EncodeCompleteCallback> callback =
     new EncodeCallback(global, promise);
 
-  CanvasRenderingContextHelper::ToBlob(aCx, global,
-                                       callback, aType, aParams, aRv);
+  // 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 if fingerprinting resistance is on.
+  bool usePlaceholder = nsContentUtils::ShouldResistFingerprinting();
+  CanvasRenderingContextHelper::ToBlob(aCx, global, callback, aType, aParams,
+                                       usePlaceholder, aRv);
 
   return promise.forget();
 }
 
 already_AddRefed<gfx::SourceSurface>
 OffscreenCanvas::GetSurfaceSnapshot(gfxAlphaType* const aOutAlphaType)
 {
   if (!mCurrentContext) {
--- a/dom/html/HTMLCanvasElement.cpp
+++ b/dom/html/HTMLCanvasElement.cpp
@@ -36,16 +36,17 @@
 #include "nsIWritablePropertyBag2.h"
 #include "nsIXPConnect.h"
 #include "nsJSUtils.h"
 #include "nsLayoutUtils.h"
 #include "nsMathUtils.h"
 #include "nsNetUtil.h"
 #include "nsRefreshDriver.h"
 #include "nsStreamUtils.h"
+#include "CanvasUtils.h"
 #include "ActiveLayerTracker.h"
 #include "VRManagerChild.h"
 #include "WebGL1Context.h"
 #include "WebGL2Context.h"
 
 using namespace mozilla::layers;
 using namespace mozilla::gfx;
 
@@ -55,26 +56,29 @@ namespace mozilla {
 namespace dom {
 
 class RequestedFrameRefreshObserver : public nsARefreshObserver
 {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RequestedFrameRefreshObserver, override)
 
 public:
   RequestedFrameRefreshObserver(HTMLCanvasElement* const aOwningElement,
-                                nsRefreshDriver* aRefreshDriver)
+                                nsRefreshDriver* aRefreshDriver,
+                                bool aReturnPlaceholderData)
     : mRegistered(false),
+      mReturnPlaceholderData(aReturnPlaceholderData),
       mOwningElement(aOwningElement),
       mRefreshDriver(aRefreshDriver)
   {
     MOZ_ASSERT(mOwningElement);
   }
 
   static already_AddRefed<DataSourceSurface>
-  CopySurface(const RefPtr<SourceSurface>& aSurface)
+  CopySurface(const RefPtr<SourceSurface>& aSurface,
+              bool aReturnPlaceholderData)
   {
     RefPtr<DataSourceSurface> data = aSurface->GetDataSurface();
     if (!data) {
       return nullptr;
     }
 
     DataSourceSurface::ScopedMap read(data, DataSourceSurface::READ);
     if (!read.IsMapped()) {
@@ -93,22 +97,33 @@ public:
     if (!write.IsMapped()) {
       return nullptr;
     }
 
     MOZ_ASSERT(read.GetStride() == write.GetStride());
     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());
 
     PROFILER_LABEL("HTMLCanvasElement", "FrameCapture", js::ProfileEntry::Category::OTHER);
 
     if (!mOwningElement) {
       return;
@@ -135,17 +150,18 @@ public:
       if (!snapshot) {
         return;
       }
     }
 
     RefPtr<DataSourceSurface> copy;
     {
       PROFILER_LABEL("HTMLCanvasElement", "CopySnapshot", js::ProfileEntry::Category::OTHER);
-      copy = CopySurface(snapshot);
+      RefPtr<DataSourceSurface> copy = CopySurface(snapshot,
+                                                   mReturnPlaceholderData);
       if (!copy) {
         return;
       }
     }
 
     {
       PROFILER_LABEL("HTMLCanvasElement", "SetFrame", js::ProfileEntry::Category::OTHER);
       mOwningElement->SetFrameCapture(copy.forget(), aTime);
@@ -191,16 +207,17 @@ public:
 private:
   virtual ~RequestedFrameRefreshObserver()
   {
     MOZ_ASSERT(!mRefreshDriver);
     MOZ_ASSERT(!mRegistered);
   }
 
   bool mRegistered;
+  bool mReturnPlaceholderData;
   HTMLCanvasElement* const mOwningElement;
   RefPtr<nsRefreshDriver> mRefreshDriver;
 };
 
 // ---------------------------------------------------------------------------
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLCanvasPrintState, mCanvas,
                                       mContext, mCallback)
@@ -751,33 +768,44 @@ HTMLCanvasElement::CaptureStream(const O
     return nullptr;
   }
 
   RefPtr<MediaStreamTrack> track =
   stream->CreateDOMTrack(videoTrackId, MediaSegment::VIDEO,
                          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;
   }
 
   return stream.forget();
 }
 
 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);
 }
 
 nsresult
 HTMLCanvasElement::ToDataURLImpl(JSContext* aCx,
                                  const nsAString& aMimeType,
@@ -797,22 +825,22 @@ HTMLCanvasElement::ToDataURLImpl(JSConte
   bool usingCustomParseOptions;
   nsresult rv =
     ParseParams(aCx, type, aEncoderOptions, params, &usingCustomParseOptions);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   nsCOMPtr<nsIInputStream> 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);
 
   // build data URL string
   aDataURL = NS_LITERAL_STRING("data:") + type + NS_LITERAL_STRING(";base64,");
 
   uint64_t count;
@@ -850,19 +878,21 @@ HTMLCanvasElement::ToBlob(JSContext* aCx
                   NewRunnableMethod<Blob*, const char*>(
                           &aCallback,
                           static_cast<void(BlobCallback::*)(
                             Blob*, const char*)>(&BlobCallback::Call),
                           nullptr, nullptr));
     return;
   }
 
+  // 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*
 HTMLCanvasElement::TransferControlToOffscreen(ErrorResult& aRv)
 {
   if (mCurrentContext) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
@@ -920,17 +950,18 @@ HTMLCanvasElement::MozGetAsFile(const ns
 
 nsresult
 HTMLCanvasElement::MozGetAsFileImpl(const nsAString& aName,
                                     const nsAString& aType,
                                     File** aResult)
 {
   nsCOMPtr<nsIInputStream> 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;
   rv = stream->Available(&imgSize);
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(imgSize <= UINT32_MAX, NS_ERROR_FILE_TOO_BIG);
 
   void* imgData = nullptr;
@@ -1206,17 +1237,18 @@ HTMLCanvasElement::MarkContextCleanForFr
 
 bool
 HTMLCanvasElement::IsContextCleanForFrameCapture()
 {
   return mCurrentContext && mCurrentContext->IsContextCleanForFrameCapture();
 }
 
 nsresult
-HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener)
+HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener,
+                                                bool aReturnPlaceholderData)
 {
   WeakPtr<FrameCaptureListener> listener = aListener;
 
   if (mRequestedFrameListeners.Contains(listener)) {
     return NS_OK;
   }
 
   if (!mRequestedFrameRefreshObserver) {
@@ -1245,17 +1277,19 @@ HTMLCanvasElement::RegisterFrameCaptureL
     }
 
     nsRefreshDriver* driver = context->RefreshDriver();
     if (!driver) {
       return NS_ERROR_FAILURE;
     }
 
     mRequestedFrameRefreshObserver =
-      new RequestedFrameRefreshObserver(this, driver);
+      new RequestedFrameRefreshObserver(this, driver, aReturnPlaceholderData);
+  } else {
+    mRequestedFrameRefreshObserver->SetReturnPlaceholderData(aReturnPlaceholderData);
   }
 
   mRequestedFrameListeners.AppendElement(listener);
   mRequestedFrameRefreshObserver->Register();
   return NS_OK;
 }
 
 bool
--- a/dom/html/HTMLCanvasElement.h
+++ b/dom/html/HTMLCanvasElement.h
@@ -259,18 +259,21 @@ public:
 
   /*
    * Register a FrameCaptureListener with this canvas.
    * The canvas hooks into the RefreshDriver while there are
    * FrameCaptureListeners registered.
    * 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
    * that has requested a frame capture.
    */
   bool IsFrameCaptureRequested() const;
 
   /*
@@ -360,17 +363,18 @@ protected:
 
   virtual JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   virtual nsIntSize GetWidthHeight() override;
 
   virtual already_AddRefed<nsICanvasRenderingContextInternal>
   CreateContext(CanvasContextType aContextType) override;
 
-  nsresult ExtractData(nsAString& aType,
+  nsresult ExtractData(JSContext* aCx,
+                       nsAString& aType,
                        const nsAString& aOptions,
                        nsIInputStream** aStream);
   nsresult ToDataURLImpl(JSContext* aCx,
                          const nsAString& aMimeType,
                          const JS::Value& aEncoderOptions,
                          nsAString& aDataURL);
   nsresult MozGetAsFileImpl(const nsAString& aName,
                             const nsAString& aType,
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -609,16 +609,24 @@ parent:
      * When the session history is across multiple root docshells, this function
      * is used to notify parent that it needs to navigate to an entry out of
      * local index of the child.
      *
      * @param aGlobalIndex The global index of history entry to navigate to.
      */
     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
      * side, with the given |visibleRect|.  This message is expected
      * to trigger creation of the remote browser's "widget".
      *
      * |Show()| and |Move()| take IntSizes rather than Rects because
      * content processes always render to a virtual <0, 0> top-left
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -3255,16 +3255,35 @@ TabParent::RecvRequestCrossBrowserNaviga
   nsCOMPtr<nsISupports> promise;
   if (NS_FAILED(frameLoader->RequestGroupedHistoryNavigation(aGlobalIndex,
                                                              getter_AddRefs(promise)))) {
     return IPC_FAIL_NO_REASON(this);
   }
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+TabParent::RecvShowCanvasPermissionPrompt(const nsCString& firstPartyURI)
+{
+  nsCOMPtr<nsIBrowser> browser = do_QueryInterface(mFrameElement);
+  if (!browser) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  if (!os) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  nsresult rv = os->NotifyObservers(browser, "canvas-permissions-prompt",
+                                    NS_ConvertUTF8toUTF16(firstPartyURI).get());
+  if (NS_FAILED(rv)) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  return IPC_OK();
+}
+
 void
 TabParent::LiveResizeStarted()
 {
   SuppressDisplayport(true);
 }
 
 void
 TabParent::LiveResizeStopped()
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -618,16 +618,17 @@ protected:
 
   virtual mozilla::ipc::IPCResult RecvGetTabCount(uint32_t* aValue) override;
 
   virtual mozilla::ipc::IPCResult RecvSHistoryUpdate(const uint32_t& aCount,
                                                      const uint32_t& aLocalIndex,
                                                      const bool& aTruncate) override;
 
   virtual mozilla::ipc::IPCResult RecvRequestCrossBrowserNavigation(const uint32_t& aGlobalIndex) override;
+  virtual mozilla::ipc::IPCResult RecvShowCanvasPermissionPrompt(const nsCString& firstPartyURI) override;
 
   ContentCacheInParent mContentCache;
 
   nsIntRect mRect;
   ScreenIntSize mDimensions;
   ScreenOrientationInternal mOrientation;
   float mDPI;
   int32_t mRounding;
--- a/dom/media/imagecapture/CaptureTask.cpp
+++ b/dom/media/imagecapture/CaptureTask.cpp
@@ -152,16 +152,17 @@ CaptureTask::SetCurrentFrames(const Vide
       nsresult rv;
       nsAutoString type(NS_LITERAL_STRING("image/jpeg"));
       nsAutoString options;
       rv = dom::ImageEncoder::ExtractDataFromLayersImageAsync(
                                 type,
                                 options,
                                 false,
                                 image,
+                                false,
                                 new EncodeComplete(this));
       if (NS_FAILED(rv)) {
         PostTrackEndEvent();
       }
       return;
     }
   }
 }