Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <stdlib.h>
#include <stdarg.h>
#include "nsICanvasRenderingContextInternal.h"
#include "nsIHTMLCollection.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "mozilla/dom/OffscreenCanvas.h"
#include "mozilla/dom/UserActivation.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/StaticPrefs_gfx.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/StaticPrefs_webgl.h"
#include "nsIPrincipal.h"
#include "nsGfxCIID.h"
#include "nsTArray.h"
#include "CanvasUtils.h"
#include "mozilla/gfx/Matrix.h"
#include "WebGL2Context.h"
#include "nsIScriptError.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 "jsapi.h"
#define TOPIC_CANVAS_PERMISSIONS_PROMPT "canvas-permissions-prompt"
#define TOPIC_CANVAS_PERMISSIONS_PROMPT_HIDE_DOORHANGER \
"canvas-permissions-prompt-hide-doorhanger"
#define PERMISSION_CANVAS_EXTRACT_DATA "canvas"_ns
using namespace mozilla::gfx;
static bool IsUnrestrictedPrincipal(nsIPrincipal& aPrincipal) {
// The system principal can always extract canvas data.
if (aPrincipal.IsSystemPrincipal()) {
return true;
}
// Allow chrome: and resource: (this especially includes PDF.js)
if (aPrincipal.SchemeIs("chrome") || aPrincipal.SchemeIs("resource")) {
return true;
}
// Allow extension principals.
return aPrincipal.GetIsAddonOrExpandedAddonPrincipal();
}
namespace mozilla::CanvasUtils {
uint32_t GetCanvasExtractDataPermission(nsIPrincipal& aPrincipal) {
if (IsUnrestrictedPrincipal(aPrincipal)) {
return true;
}
nsresult rv;
nsCOMPtr<nsIPermissionManager> permissionManager =
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, false);
uint32_t permission;
rv = permissionManager->TestPermissionFromPrincipal(
&aPrincipal, PERMISSION_CANVAS_EXTRACT_DATA, &permission);
NS_ENSURE_SUCCESS(rv, false);
return permission;
}
bool IsImageExtractionAllowed(dom::Document* aDocument, JSContext* aCx,
nsIPrincipal& aPrincipal) {
if (NS_WARN_IF(!aDocument)) {
return false;
}
/*
* There are three RFPTargets that change the behavior here, and they can be
* in any combination
* - CanvasImageExtractionPrompt - whether or not to prompt the user for
* canvas extraction. If enabled, before canvas is extracted we will ensure
* the user has granted permission.
* - CanvasExtractionBeforeUserInputIsBlocked - if enabled, canvas extraction
* before user input has occurred is always blocked, regardless of any other
* Target behavior
* - CanvasExtractionFromThirdPartiesIsBlocked - if enabled, canvas extraction
* by third parties is always blocked, regardless of any other Target behavior
*
* There are two odd cases:
* 1) When CanvasImageExtractionPrompt=false but
* CanvasExtractionBeforeUserInputIsBlocked=true Conceptually this is
* "Always allow canvas extraction in response to user input, and never
* allow it otherwise"
*
* That's fine as a concept, but it might be a little confusing, so we
* still want to show the permission icon in the address bar, but never
* the permission doorhanger.
* 2) When CanvasExtractionFromThirdPartiesIsBlocked=false - we will prompt
* the user for permission _for the frame_ (maybe with the doorhanger,
* maybe not). The prompt shows the frame's origin, but it's easy to
* mistake that for the origin of the top-level page and grant it when you
* don't mean to. This combination isn't likely to be used by anyone
* except those opting in, so that's alright.
*/
// We can improve this mechanism when we have this implemented as a bitset
if (!aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt) &&
!aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasExtractionBeforeUserInputIsBlocked) &&
!aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasExtractionFromThirdPartiesIsBlocked)) {
return true;
}
// -------------------------------------------------------------------
// General Exemptions
// Don't proceed if we don't have a document or JavaScript context.
if (!aCx) {
return false;
}
// The system and extension principals can always extract canvas data.
if (IsUnrestrictedPrincipal(aPrincipal)) {
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.
if (docURI->SchemeIs("file")) {
return true;
}
// -------------------------------------------------------------------
// Possibly block third parties
if (aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasExtractionFromThirdPartiesIsBlocked)) {
MOZ_ASSERT(aDocument->GetWindowContext());
bool isThirdParty =
aDocument->GetWindowContext()
? aDocument->GetWindowContext()->GetIsThirdPartyWindow()
: false;
if (isThirdParty) {
nsAutoString message;
message.AppendPrintf(
"Blocked third party %s from extracting canvas data.",
docURISpec.get());
nsContentUtils::ReportToConsoleNonLocalized(
message, nsIScriptError::warningFlag, "Security"_ns, aDocument);
return false;
}
}
// -------------------------------------------------------------------
// Check if we will do any further blocking
if (!aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt) &&
!aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasExtractionBeforeUserInputIsBlocked)) {
return true;
}
// -------------------------------------------------------------------
// Check a site's permission
// If the user has previously granted or not granted permission, we can return
// immediately. Load Permission Manager service.
uint64_t permission = GetCanvasExtractDataPermission(aPrincipal);
switch (permission) {
case nsIPermissionManager::ALLOW_ACTION:
return true;
case nsIPermissionManager::DENY_ACTION:
return false;
default:
break;
}
// -------------------------------------------------------------------
// At this point, there's only one way to return true: if we are always
// allowing canvas in response to user input, and not prompting
bool hidePermissionDoorhanger = false;
if (!aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt) &&
aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasExtractionBeforeUserInputIsBlocked)) {
// If so, see if this is in response to user input.
if (dom::UserActivation::IsHandlingUserInput()) {
return true;
}
hidePermissionDoorhanger = true;
}
// -------------------------------------------------------------------
// Now we know we're going to block it, and log something to the console,
// and show some sort of prompt maybe with the doorhanger, maybe not
hidePermissionDoorhanger |=
aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasExtractionBeforeUserInputIsBlocked) &&
!dom::UserActivation::IsHandlingUserInput();
if (hidePermissionDoorhanger) {
nsAutoString message;
message.AppendPrintf(
"Blocked %s from extracting canvas data because no user input was "
"detected.",
docURISpec.get());
nsContentUtils::ReportToConsoleNonLocalized(
message, nsIScriptError::warningFlag, "Security"_ns, aDocument);
} else {
// It was in response to user input, so log and display the prompt.
nsAutoString message;
message.AppendPrintf(
"Blocked %s from extracting canvas data, but prompting the user.",
docURISpec.get());
nsContentUtils::ReportToConsoleNonLocalized(
message, nsIScriptError::warningFlag, "Security"_ns, aDocument);
}
// Show the prompt to the user (asynchronous) - maybe with the doorhanger,
// maybe not
nsPIDOMWindowOuter* win = aDocument->GetWindow();
nsAutoCString origin;
nsresult rv = aPrincipal.GetOrigin(origin);
NS_ENSURE_SUCCESS(rv, false);
if (XRE_IsContentProcess()) {
dom::BrowserChild* browserChild = dom::BrowserChild::GetFrom(win);
if (browserChild) {
browserChild->SendShowCanvasPermissionPrompt(origin,
hidePermissionDoorhanger);
}
} else {
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->NotifyObservers(win,
hidePermissionDoorhanger
? TOPIC_CANVAS_PERMISSIONS_PROMPT_HIDE_DOORHANGER
: TOPIC_CANVAS_PERMISSIONS_PROMPT,
NS_ConvertUTF8toUTF16(origin).get());
}
}
// We don't extract the image for now -- user may override at prompt.
return false;
}
ImageExtraction ImageExtractionResult(dom::HTMLCanvasElement* aCanvasElement,
JSContext* aCx,
nsIPrincipal& aPrincipal) {
if (IsUnrestrictedPrincipal(aPrincipal)) {
return ImageExtraction::Unrestricted;
}
nsCOMPtr<dom::Document> ownerDoc = aCanvasElement->OwnerDoc();
if (!IsImageExtractionAllowed(ownerDoc, aCx, aPrincipal)) {
return ImageExtraction::Placeholder;
}
if (ownerDoc->ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
if (GetCanvasExtractDataPermission(aPrincipal) ==
nsIPermissionManager::ALLOW_ACTION) {
return ImageExtraction::Unrestricted;
}
return ImageExtraction::Randomize;
}
return ImageExtraction::Unrestricted;
}
ImageExtraction ImageExtractionResult(dom::OffscreenCanvas* aOffscreenCanvas,
JSContext* aCx,
nsIPrincipal& aPrincipal) {
if (IsUnrestrictedPrincipal(aPrincipal)) {
return ImageExtraction::Unrestricted;
}
if (aOffscreenCanvas->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt)) {
return ImageExtraction::Placeholder;
}
if (aOffscreenCanvas->ShouldResistFingerprinting(
RFPTarget::CanvasRandomization)) {
return ImageExtraction::Randomize;
}
return ImageExtraction::Unrestricted;
}
bool GetCanvasContextType(const nsAString& str,
dom::CanvasContextType* const out_type) {
if (str.EqualsLiteral("2d")) {
*out_type = dom::CanvasContextType::Canvas2D;
return true;
}
if (str.EqualsLiteral("webgl") || str.EqualsLiteral("experimental-webgl")) {
*out_type = dom::CanvasContextType::WebGL1;
return true;
}
if (StaticPrefs::webgl_enable_webgl2()) {
if (str.EqualsLiteral("webgl2")) {
*out_type = dom::CanvasContextType::WebGL2;
return true;
}
}
if (gfxVars::AllowWebGPU()) {
if (str.EqualsLiteral("webgpu")) {
*out_type = dom::CanvasContextType::WebGPU;
return true;
}
}
if (str.EqualsLiteral("bitmaprenderer")) {
*out_type = dom::CanvasContextType::ImageBitmap;
return true;
}
return false;
}
/**
* This security check utility might be called from an source that never taints
* others. For example, while painting a CanvasPattern, which is created from an
* ImageBitmap, onto a canvas. In this case, the caller could set the CORSUsed
* true in order to pass this check and leave the aPrincipal to be a nullptr
* since the aPrincipal is not going to be used.
*/
void DoDrawImageSecurityCheck(dom::HTMLCanvasElement* aCanvasElement,
nsIPrincipal* aPrincipal, bool forceWriteOnly,
bool CORSUsed) {
// Callers should ensure that mCanvasElement is non-null before calling this
if (!aCanvasElement) {
NS_WARNING("DoDrawImageSecurityCheck called without canvas element!");
return;
}
if (aCanvasElement->IsWriteOnly() && !aCanvasElement->mExpandedReader) {
return;
}
// If we explicitly set WriteOnly just do it and get out
if (forceWriteOnly) {
aCanvasElement->SetWriteOnly();
return;
}
// No need to do a security check if the image used CORS for the load
if (CORSUsed) return;
if (NS_WARN_IF(!aPrincipal)) {
MOZ_ASSERT_UNREACHABLE("Must have a principal here");
aCanvasElement->SetWriteOnly();
return;
}
if (aCanvasElement->NodePrincipal()->Subsumes(aPrincipal)) {
// This canvas has access to that image anyway
return;
}
if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
// This is a resource from an extension content script principal.
if (aCanvasElement->mExpandedReader &&
aCanvasElement->mExpandedReader->Subsumes(aPrincipal)) {
// This canvas already allows reading from this principal.
return;
}
if (!aCanvasElement->mExpandedReader) {
// Allow future reads from this same princial only.
aCanvasElement->SetWriteOnly(aPrincipal);
return;
}
// If we got here, this must be the *second* extension tainting
// the canvas. Fall through to mark it WriteOnly for everyone.
}
aCanvasElement->SetWriteOnly();
}
/**
* This security check utility might be called from an source that never taints
* others. For example, while painting a CanvasPattern, which is created from an
* ImageBitmap, onto a canvas. In this case, the caller could set the aCORSUsed
* true in order to pass this check and leave the aPrincipal to be a nullptr
* since the aPrincipal is not going to be used.
*/
void DoDrawImageSecurityCheck(dom::OffscreenCanvas* aOffscreenCanvas,
nsIPrincipal* aPrincipal, bool aForceWriteOnly,
bool aCORSUsed) {
// Callers should ensure that mCanvasElement is non-null before calling this
if (NS_WARN_IF(!aOffscreenCanvas)) {
return;
}
nsIPrincipal* expandedReader = aOffscreenCanvas->GetExpandedReader();
if (aOffscreenCanvas->IsWriteOnly() && !expandedReader) {
return;
}
// If we explicitly set WriteOnly just do it and get out
if (aForceWriteOnly) {
aOffscreenCanvas->SetWriteOnly();
return;
}
// No need to do a security check if the image used CORS for the load
if (aCORSUsed) {
return;
}
// If we are on a worker thread, we might not have any principals at all.
nsIGlobalObject* global = aOffscreenCanvas->GetOwnerGlobal();
nsIPrincipal* canvasPrincipal = global ? global->PrincipalOrNull() : nullptr;
if (!aPrincipal || !canvasPrincipal) {
aOffscreenCanvas->SetWriteOnly();
return;
}
if (canvasPrincipal->Subsumes(aPrincipal)) {
// This canvas has access to that image anyway
return;
}
if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
// This is a resource from an extension content script principal.
if (expandedReader && expandedReader->Subsumes(aPrincipal)) {
// This canvas already allows reading from this principal.
return;
}
if (!expandedReader) {
// Allow future reads from this same princial only.
aOffscreenCanvas->SetWriteOnly(aPrincipal);
return;
}
// If we got here, this must be the *second* extension tainting
// the canvas. Fall through to mark it WriteOnly for everyone.
}
aOffscreenCanvas->SetWriteOnly();
}
bool CoerceDouble(const JS::Value& v, double* d) {
if (v.isDouble()) {
*d = v.toDouble();
} else if (v.isInt32()) {
*d = double(v.toInt32());
} else if (v.isUndefined()) {
*d = 0.0;
} else {
return false;
}
return true;
}
bool HasDrawWindowPrivilege(JSContext* aCx, JSObject* /* unused */) {
return nsContentUtils::CallerHasPermission(aCx,
nsGkAtoms::all_urlsPermission);
}
bool CheckWriteOnlySecurity(bool aCORSUsed, nsIPrincipal* aPrincipal,
bool aHadCrossOriginRedirects) {
if (!aPrincipal) {
return true;
}
if (!aCORSUsed) {
if (aHadCrossOriginRedirects) {
return true;
}
nsIGlobalObject* incumbentSettingsObject = dom::GetIncumbentGlobal();
if (!incumbentSettingsObject) {
return true;
}
nsIPrincipal* principal = incumbentSettingsObject->PrincipalOrNull();
if (NS_WARN_IF(!principal) || !(principal->Subsumes(aPrincipal))) {
return true;
}
}
return false;
}
} // namespace mozilla::CanvasUtils