/* -*- 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 #include #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 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 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 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