Bug 967895 - Ask for placeholder data when image extraction is not allowed (Tor 6253). draft
authorChung-Sheng Fu <cfu@mozilla.com>
Tue, 22 Aug 2017 14:23:41 +0800
changeset 679933 57204561532103e422e45ede5f7077fb849232df
parent 679804 c3f4cac45d47bf4bbc8b3f07f343cdb0e42a96cd
child 679934 56f72527fee298fe1d1d30a6cb195ec05b4c3ca5
push id84348
push userbmo:cfu@mozilla.com
push dateFri, 13 Oct 2017 07:50:40 +0000
bugs967895
milestone58.0a1
Bug 967895 - Ask for placeholder data when image extraction is not allowed (Tor 6253). MozReview-Commit-ID: AJ5F6M5S83U
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
@@ -119,17 +119,17 @@ DOMInterfaces = {
 
 'CacheStorage': {
     'implicitJSContext': [ 'match' ],
     '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
@@ -4900,57 +4900,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,
@@ -4962,17 +4976,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;
@@ -5803,36 +5817,53 @@ CanvasRenderingContext2D::GetImageDataAr
 
   if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   IntRect dstWriteRect = srcReadRect;
   dstWriteRect.MoveBy(-aX, -aY);
 
-  {
+  // Check for site-specific 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);
+  }
+
+  do {
     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;
+
+    // Return all-white, opaque pixel data if no permission.
+    if (usePlaceholder) {
+      memset(data, 0xFF, len.value());
+      break;
+    }
+
     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,
                         dst, aWidth * 4, SurfaceFormat::R8G8B8A8,
                         dstWriteRect.Size());
     }
-  }
+  } while (false);
 
   readback->Unmap();
   *aRetval = darray;
   return NS_OK;
 }
 
 void
 CanvasRenderingContext2D::EnsureErrorTarget()
--- 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)
@@ -53,25 +54,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);
@@ -102,16 +104,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 aUsePlaceholder, ErrorResult& aRv);
 
   void ToBlob(JSContext* aCx, nsIGlobalObject* aGlobal, EncodeCompleteCallback* aCallback,
               const nsAString& aType, JS::Handle<JS::Value> aParams,
-              ErrorResult& aRv);
+              bool aUsePlaceholder, 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,171 @@
 
 #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"
 
+#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"
+
 using namespace mozilla::gfx;
 
 namespace mozilla {
 namespace CanvasUtils {
 
+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, &rv);
+    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);
+    switch (permission) {
+    case nsIPermissionManager::ALLOW_ACTION:
+        return true;
+    case nsIPermissionManager::DENY_ACTION:
+        return false;
+    default:
+        break;
+    }
+
+    // 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,19 @@ 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);
 
+// Check site-specific permission and display prompt if appropriate.
+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
@@ -279,18 +279,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
@@ -39,16 +39,17 @@
 #include "nsIXPConnect.h"
 #include "nsJSUtils.h"
 #include "nsLayoutUtils.h"
 #include "nsMathUtils.h"
 #include "nsNetUtil.h"
 #include "nsRefreshDriver.h"
 #include "nsStreamUtils.h"
 #include "ActiveLayerTracker.h"
+#include "CanvasUtils.h"
 #include "VRManagerChild.h"
 #include "WebGL1Context.h"
 #include "WebGL2Context.h"
 
 using namespace mozilla::layers;
 using namespace mozilla::gfx;
 
 NS_IMPL_NS_NEW_HTML_ELEMENT(Canvas)
@@ -57,26 +58,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()) {
@@ -95,22 +99,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());
 
     AUTO_PROFILER_LABEL("RequestedFrameRefreshObserver::WillRefresh", OTHER);
 
     if (!mOwningElement) {
       return;
@@ -139,17 +154,17 @@ public:
         return;
       }
     }
 
     RefPtr<DataSourceSurface> copy;
     {
       AUTO_PROFILER_LABEL(
         "RequestedFrameRefreshObserver::WillRefresh:CopySurface", OTHER);
-      copy = CopySurface(snapshot);
+      copy = CopySurface(snapshot, mReturnPlaceholderData);
       if (!copy) {
         return;
       }
     }
 
     {
       AUTO_PROFILER_LABEL(
         "RequestedFrameRefreshObserver::WillRefresh:SetFrame", OTHER);
@@ -196,16 +211,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)
@@ -749,33 +765,45 @@ 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,
@@ -795,22 +823,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,18 +878,21 @@ HTMLCanvasElement::ToBlob(JSContext* aCx
         &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);
@@ -920,17 +951,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;
@@ -1241,17 +1273,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) {
@@ -1280,17 +1313,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
@@ -257,17 +257,18 @@ 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.
    */
-  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;
 
   /*
@@ -343,17 +344,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
@@ -589,16 +589,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
@@ -3599,16 +3599,37 @@ 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& aFirstPartyURI)
+{
+  nsCOMPtr<nsIBrowser> 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 IPC_OK();
+  }
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  if (!os) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  nsresult rv = os->NotifyObservers(browser, "canvas-permissions-prompt",
+                                    NS_ConvertUTF8toUTF16(aFirstPartyURI).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
@@ -643,16 +643,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& aFirstPartyURI) 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;
     }
   }
 }