Bug 967895 - Ask for placeholder data when image extraction is not allowed (Tor 6253). r=jrmuizel

MozReview-Commit-ID: AJ5F6M5S83U

--HG--
extra : rebase_source : 894b16575ebbccc26c5b639d7526cb473501d9d2
This commit is contained in:
Chung-Sheng Fu 2017-08-22 14:23:41 +08:00
parent de8dc32c1d
commit 6eb3e9c2c9
14 changed files with 281 additions and 31 deletions

View File

@ -124,7 +124,7 @@ DOMInterfaces = {
'CanvasRenderingContext2D': {
'implicitJSContext': [
'createImageData', 'getImageData'
'createImageData', 'getImageData', 'isPointInPath', 'isPointInStroke'
],
'binaryNames': {
'mozImageSmoothingEnabled': 'imageSmoothingEnabled'

View File

@ -4905,12 +4905,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<nsIDocument> ownerDoc = mCanvasElement->OwnerDoc();
if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx))
return false;
}
EnsureUserSpacePath(aWinding);
if (!mPath) {
return false;
@ -4923,7 +4930,7 @@ 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;
@ -4940,12 +4947,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<nsIDocument> ownerDoc = mCanvasElement->OwnerDoc();
if (!ownerDoc || !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx))
return false;
}
EnsureUserSpacePath();
if (!mPath) {
return false;
@ -4967,7 +4981,7 @@ 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;
@ -5808,7 +5822,17 @@ CanvasRenderingContext2D::GetImageDataArray(JSContext* aCx,
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);
@ -5816,6 +5840,13 @@ CanvasRenderingContext2D::GetImageDataArray(JSContext* aCx,
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) {
@ -5827,7 +5858,7 @@ CanvasRenderingContext2D::GetImageDataArray(JSContext* aCx,
dst, aWidth * 4, SurfaceFormat::R8G8B8A8,
dstWriteRect.Size());
}
}
} while (false);
readback->Unmap();
*aRetval = darray;

View File

@ -204,10 +204,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<double>& aMaxWidth,
mozilla::ErrorResult& aError);

View File

@ -25,6 +25,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
BlobCallback& aCallback,
const nsAString& aType,
JS::Handle<JS::Value> aParams,
bool aUsePlaceholder,
ErrorResult& aRv)
{
// Encoder callback when encoding is complete.
@ -58,7 +59,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
RefPtr<EncodeCompleteCallback> callback =
new EncodeCallback(aGlobal, &aCallback);
ToBlob(aCx, aGlobal, callback, aType, aParams, aRv);
ToBlob(aCx, aGlobal, callback, aType, aParams, aUsePlaceholder, aRv);
}
void
@ -67,6 +68,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
EncodeCompleteCallback* aCallback,
const nsAString& aType,
JS::Handle<JS::Value> aParams,
bool aUsePlaceholder,
ErrorResult& aRv)
{
nsAutoString type;
@ -107,6 +109,7 @@ CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
Move(imageBuffer),
format,
GetWidthHeight(),
aUsePlaceholder,
callback);
}

View File

@ -58,11 +58,11 @@ protected:
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);

View File

@ -13,6 +13,7 @@
#include "nsICanvasRenderingContextInternal.h"
#include "nsIHTMLCollection.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "mozilla/dom/TabChild.h"
#include "nsIPrincipal.h"
#include "nsGfxCIID.h"
@ -23,11 +24,150 @@
#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)
{

View File

@ -49,6 +49,9 @@ void DoDrawImageSecurityCheck(dom::HTMLCanvasElement *aCanvasElement,
// 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.

View File

@ -286,8 +286,13 @@ OffscreenCanvas::ToBlob(JSContext* aCx,
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();
}

View File

@ -44,6 +44,7 @@
#include "nsRefreshDriver.h"
#include "nsStreamUtils.h"
#include "ActiveLayerTracker.h"
#include "CanvasUtils.h"
#include "VRManagerChild.h"
#include "WebGL1Context.h"
#include "WebGL2Context.h"
@ -62,8 +63,10 @@ class RequestedFrameRefreshObserver : public nsARefreshObserver
public:
RequestedFrameRefreshObserver(HTMLCanvasElement* const aOwningElement,
nsRefreshDriver* aRefreshDriver)
nsRefreshDriver* aRefreshDriver,
bool aReturnPlaceholderData)
: mRegistered(false),
mReturnPlaceholderData(aReturnPlaceholderData),
mOwningElement(aOwningElement),
mRefreshDriver(aRefreshDriver)
{
@ -71,7 +74,8 @@ public:
}
static already_AddRefed<DataSourceSurface>
CopySurface(const RefPtr<SourceSurface>& aSurface)
CopySurface(const RefPtr<SourceSurface>& aSurface,
bool aReturnPlaceholderData)
{
RefPtr<DataSourceSurface> data = aSurface->GetDataSurface();
if (!data) {
@ -100,12 +104,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());
@ -144,7 +159,7 @@ public:
{
AUTO_PROFILER_LABEL(
"RequestedFrameRefreshObserver::WillRefresh:CopySurface", OTHER);
copy = CopySurface(snapshot);
copy = CopySurface(snapshot, mReturnPlaceholderData);
if (!copy) {
return;
}
@ -201,6 +216,7 @@ private:
}
bool mRegistered;
bool mReturnPlaceholderData;
HTMLCanvasElement* const mOwningElement;
RefPtr<nsRefreshDriver> mRefreshDriver;
};
@ -754,7 +770,14 @@ HTMLCanvasElement::CaptureStream(const Optional<double>& 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;
@ -764,13 +787,18 @@ HTMLCanvasElement::CaptureStream(const Optional<double>& 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);
@ -800,12 +828,12 @@ HTMLCanvasElement::ToDataURLImpl(JSContext* aCx,
}
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);
@ -855,8 +883,11 @@ HTMLCanvasElement::ToBlob(JSContext* aCx,
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);
}
@ -925,7 +956,8 @@ HTMLCanvasElement::MozGetAsFileImpl(const nsAString& aName,
{
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;
@ -1246,7 +1278,8 @@ HTMLCanvasElement::IsContextCleanForFrameCapture()
}
nsresult
HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener)
HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener,
bool aReturnPlaceholderData)
{
WeakPtr<FrameCaptureListener> listener = aListener;
@ -1285,7 +1318,9 @@ HTMLCanvasElement::RegisterFrameCaptureListener(FrameCaptureListener* aListener)
}
mRequestedFrameRefreshObserver =
new RequestedFrameRefreshObserver(this, driver);
new RequestedFrameRefreshObserver(this, driver, aReturnPlaceholderData);
} else {
mRequestedFrameRefreshObserver->SetReturnPlaceholderData(aReturnPlaceholderData);
}
mRequestedFrameListeners.AppendElement(listener);

View File

@ -262,7 +262,8 @@ public:
* 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
@ -348,7 +349,8 @@ protected:
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,

View File

@ -594,6 +594,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

View File

@ -3604,6 +3604,27 @@ TabParent::RecvRequestCrossBrowserNavigation(const uint32_t& aGlobalIndex)
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()
{

View File

@ -648,6 +648,7 @@ protected:
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;

View File

@ -157,6 +157,7 @@ CaptureTask::SetCurrentFrames(const VideoSegment& aSegment)
options,
false,
image,
false,
new EncodeComplete(this));
if (NS_FAILED(rv)) {
PostTrackEndEvent();