diff options
Diffstat (limited to 'dom/canvas/CanvasUtils.cpp')
-rw-r--r-- | dom/canvas/CanvasUtils.cpp | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/dom/canvas/CanvasUtils.cpp b/dom/canvas/CanvasUtils.cpp new file mode 100644 index 0000000000..69f185cc20 --- /dev/null +++ b/dom/canvas/CanvasUtils.cpp @@ -0,0 +1,471 @@ +/* -*- 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; + +namespace mozilla::CanvasUtils { + +bool IsImageExtractionAllowed(dom::Document* aDocument, JSContext* aCx, + Maybe<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 || !aPrincipal) { + return false; + } + + nsIPrincipal& subjectPrincipal = *aPrincipal.ref(); + + // The system principal can always extract canvas data. + if (subjectPrincipal.IsSystemPrincipal()) { + return true; + } + + // Allow extension principals. + auto* principal = BasePrincipal::Cast(&subjectPrincipal); + if (principal->AddonPolicy() || principal->ContentScriptAddonPolicy()) { + 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; + } + + // Don't show canvas prompt for PDF.js + JS::AutoFilename scriptFile; + if (JS::DescribeScriptedCaller(aCx, &scriptFile) && scriptFile.get() && + strcmp(scriptFile.get(), "resource://pdf.js/build/pdf.js") == 0) { + 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. + nsresult rv; + 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->TestPermissionFromPrincipal( + principal, 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, 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) && + StaticPrefs:: + privacy_resistFingerprinting_autoDeclineNoUserInputCanvasPrompts() && + 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 |= + StaticPrefs:: + privacy_resistFingerprinting_autoDeclineNoUserInputCanvasPrompts() && + 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; + rv = principal->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; +} + +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 |