Bug 1850713: remove duplicated setting of early hint preloader id in `ScriptLoader...
[gecko.git] / dom / canvas / CanvasUtils.cpp
blobb24ff9742acb1fedb9262ff9fb746851158c7702
1 /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include <stdlib.h>
7 #include <stdarg.h>
9 #include "nsICanvasRenderingContextInternal.h"
10 #include "nsIHTMLCollection.h"
11 #include "mozilla/dom/BrowserChild.h"
12 #include "mozilla/dom/Document.h"
13 #include "mozilla/dom/HTMLCanvasElement.h"
14 #include "mozilla/dom/OffscreenCanvas.h"
15 #include "mozilla/dom/UserActivation.h"
16 #include "mozilla/dom/WorkerCommon.h"
17 #include "mozilla/dom/WorkerPrivate.h"
18 #include "mozilla/gfx/gfxVars.h"
19 #include "mozilla/BasePrincipal.h"
20 #include "mozilla/StaticPrefs_gfx.h"
21 #include "mozilla/StaticPrefs_privacy.h"
22 #include "mozilla/StaticPrefs_webgl.h"
23 #include "nsIPrincipal.h"
25 #include "nsGfxCIID.h"
27 #include "nsTArray.h"
29 #include "CanvasUtils.h"
30 #include "mozilla/gfx/Matrix.h"
31 #include "WebGL2Context.h"
33 #include "nsIScriptError.h"
34 #include "nsIScriptObjectPrincipal.h"
35 #include "nsIPermissionManager.h"
36 #include "nsIObserverService.h"
37 #include "mozilla/Services.h"
38 #include "mozIThirdPartyUtil.h"
39 #include "nsContentUtils.h"
40 #include "nsUnicharUtils.h"
41 #include "nsPrintfCString.h"
42 #include "jsapi.h"
44 #define TOPIC_CANVAS_PERMISSIONS_PROMPT "canvas-permissions-prompt"
45 #define TOPIC_CANVAS_PERMISSIONS_PROMPT_HIDE_DOORHANGER \
46 "canvas-permissions-prompt-hide-doorhanger"
47 #define PERMISSION_CANVAS_EXTRACT_DATA "canvas"_ns
49 using namespace mozilla::gfx;
51 namespace mozilla::CanvasUtils {
53 bool IsImageExtractionAllowed(dom::Document* aDocument, JSContext* aCx,
54 nsIPrincipal& aPrincipal) {
55 if (NS_WARN_IF(!aDocument)) {
56 return false;
60 * There are three RFPTargets that change the behavior here, and they can be
61 * in any combination
62 * - CanvasImageExtractionPrompt - whether or not to prompt the user for
63 * canvas extraction. If enabled, before canvas is extracted we will ensure
64 * the user has granted permission.
65 * - CanvasExtractionBeforeUserInputIsBlocked - if enabled, canvas extraction
66 * before user input has occurred is always blocked, regardless of any other
67 * Target behavior
68 * - CanvasExtractionFromThirdPartiesIsBlocked - if enabled, canvas extraction
69 * by third parties is always blocked, regardless of any other Target behavior
71 * There are two odd cases:
72 * 1) When CanvasImageExtractionPrompt=false but
73 * CanvasExtractionBeforeUserInputIsBlocked=true Conceptually this is
74 * "Always allow canvas extraction in response to user input, and never
75 * allow it otherwise"
77 * That's fine as a concept, but it might be a little confusing, so we
78 * still want to show the permission icon in the address bar, but never
79 * the permission doorhanger.
80 * 2) When CanvasExtractionFromThirdPartiesIsBlocked=false - we will prompt
81 * the user for permission _for the frame_ (maybe with the doorhanger,
82 * maybe not). The prompt shows the frame's origin, but it's easy to
83 * mistake that for the origin of the top-level page and grant it when you
84 * don't mean to. This combination isn't likely to be used by anyone
85 * except those opting in, so that's alright.
88 // We can improve this mechanism when we have this implemented as a bitset
89 if (!aDocument->ShouldResistFingerprinting(
90 RFPTarget::CanvasImageExtractionPrompt) &&
91 !aDocument->ShouldResistFingerprinting(
92 RFPTarget::CanvasExtractionBeforeUserInputIsBlocked) &&
93 !aDocument->ShouldResistFingerprinting(
94 RFPTarget::CanvasExtractionFromThirdPartiesIsBlocked)) {
95 return true;
98 // -------------------------------------------------------------------
99 // General Exemptions
101 // Don't proceed if we don't have a document or JavaScript context.
102 if (!aCx) {
103 return false;
106 // The system principal can always extract canvas data.
107 if (aPrincipal.IsSystemPrincipal()) {
108 return true;
111 // Allow extension principals.
112 auto* principal = BasePrincipal::Cast(&aPrincipal);
113 if (principal->AddonPolicy() || principal->ContentScriptAddonPolicy()) {
114 return true;
117 // Get the document URI and its spec.
118 nsIURI* docURI = aDocument->GetDocumentURI();
119 nsCString docURISpec;
120 docURI->GetSpec(docURISpec);
122 // Allow local files to extract canvas data.
123 if (docURI->SchemeIs("file")) {
124 return true;
127 // Don't show canvas prompt for PDF.js
128 JS::AutoFilename scriptFile;
129 if (JS::DescribeScriptedCaller(aCx, &scriptFile) && scriptFile.get() &&
130 strcmp(scriptFile.get(), "resource://pdf.js/build/pdf.js") == 0) {
131 return true;
134 // -------------------------------------------------------------------
135 // Possibly block third parties
137 if (aDocument->ShouldResistFingerprinting(
138 RFPTarget::CanvasExtractionFromThirdPartiesIsBlocked)) {
139 MOZ_ASSERT(aDocument->GetWindowContext());
140 bool isThirdParty =
141 aDocument->GetWindowContext()
142 ? aDocument->GetWindowContext()->GetIsThirdPartyWindow()
143 : false;
144 if (isThirdParty) {
145 nsAutoString message;
146 message.AppendPrintf(
147 "Blocked third party %s from extracting canvas data.",
148 docURISpec.get());
149 nsContentUtils::ReportToConsoleNonLocalized(
150 message, nsIScriptError::warningFlag, "Security"_ns, aDocument);
151 return false;
155 // -------------------------------------------------------------------
156 // Check if we will do any further blocking
158 if (!aDocument->ShouldResistFingerprinting(
159 RFPTarget::CanvasImageExtractionPrompt) &&
160 !aDocument->ShouldResistFingerprinting(
161 RFPTarget::CanvasExtractionBeforeUserInputIsBlocked)) {
162 return true;
165 // -------------------------------------------------------------------
166 // Check a site's permission
168 // If the user has previously granted or not granted permission, we can return
169 // immediately. Load Permission Manager service.
170 nsresult rv;
171 nsCOMPtr<nsIPermissionManager> permissionManager =
172 do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
173 NS_ENSURE_SUCCESS(rv, false);
175 // Check if the site has permission to extract canvas data.
176 // Either permit or block extraction if a stored permission setting exists.
177 uint32_t permission;
178 rv = permissionManager->TestPermissionFromPrincipal(
179 principal, PERMISSION_CANVAS_EXTRACT_DATA, &permission);
180 NS_ENSURE_SUCCESS(rv, false);
181 switch (permission) {
182 case nsIPermissionManager::ALLOW_ACTION:
183 return true;
184 case nsIPermissionManager::DENY_ACTION:
185 return false;
186 default:
187 break;
190 // -------------------------------------------------------------------
191 // At this point, there's only one way to return true: if we are always
192 // allowing canvas in response to user input, and not prompting
193 bool hidePermissionDoorhanger = false;
194 if (!aDocument->ShouldResistFingerprinting(
195 RFPTarget::CanvasImageExtractionPrompt) &&
196 StaticPrefs::
197 privacy_resistFingerprinting_autoDeclineNoUserInputCanvasPrompts() &&
198 aDocument->ShouldResistFingerprinting(
199 RFPTarget::CanvasExtractionBeforeUserInputIsBlocked)) {
200 // If so, see if this is in response to user input.
201 if (dom::UserActivation::IsHandlingUserInput()) {
202 return true;
205 hidePermissionDoorhanger = true;
208 // -------------------------------------------------------------------
209 // Now we know we're going to block it, and log something to the console,
210 // and show some sort of prompt maybe with the doorhanger, maybe not
212 hidePermissionDoorhanger |=
213 StaticPrefs::
214 privacy_resistFingerprinting_autoDeclineNoUserInputCanvasPrompts() &&
215 aDocument->ShouldResistFingerprinting(
216 RFPTarget::CanvasExtractionBeforeUserInputIsBlocked) &&
217 !dom::UserActivation::IsHandlingUserInput();
219 if (hidePermissionDoorhanger) {
220 nsAutoString message;
221 message.AppendPrintf(
222 "Blocked %s from extracting canvas data because no user input was "
223 "detected.",
224 docURISpec.get());
225 nsContentUtils::ReportToConsoleNonLocalized(
226 message, nsIScriptError::warningFlag, "Security"_ns, aDocument);
227 } else {
228 // It was in response to user input, so log and display the prompt.
229 nsAutoString message;
230 message.AppendPrintf(
231 "Blocked %s from extracting canvas data, but prompting the user.",
232 docURISpec.get());
233 nsContentUtils::ReportToConsoleNonLocalized(
234 message, nsIScriptError::warningFlag, "Security"_ns, aDocument);
237 // Show the prompt to the user (asynchronous) - maybe with the doorhanger,
238 // maybe not
239 nsPIDOMWindowOuter* win = aDocument->GetWindow();
240 nsAutoCString origin;
241 rv = principal->GetOrigin(origin);
242 NS_ENSURE_SUCCESS(rv, false);
244 if (XRE_IsContentProcess()) {
245 dom::BrowserChild* browserChild = dom::BrowserChild::GetFrom(win);
246 if (browserChild) {
247 browserChild->SendShowCanvasPermissionPrompt(origin,
248 hidePermissionDoorhanger);
250 } else {
251 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
252 if (obs) {
253 obs->NotifyObservers(win,
254 hidePermissionDoorhanger
255 ? TOPIC_CANVAS_PERMISSIONS_PROMPT_HIDE_DOORHANGER
256 : TOPIC_CANVAS_PERMISSIONS_PROMPT,
257 NS_ConvertUTF8toUTF16(origin).get());
261 // We don't extract the image for now -- user may override at prompt.
262 return false;
265 bool GetCanvasContextType(const nsAString& str,
266 dom::CanvasContextType* const out_type) {
267 if (str.EqualsLiteral("2d")) {
268 *out_type = dom::CanvasContextType::Canvas2D;
269 return true;
272 if (str.EqualsLiteral("webgl") || str.EqualsLiteral("experimental-webgl")) {
273 *out_type = dom::CanvasContextType::WebGL1;
274 return true;
277 if (StaticPrefs::webgl_enable_webgl2()) {
278 if (str.EqualsLiteral("webgl2")) {
279 *out_type = dom::CanvasContextType::WebGL2;
280 return true;
284 if (gfxVars::AllowWebGPU()) {
285 if (str.EqualsLiteral("webgpu")) {
286 *out_type = dom::CanvasContextType::WebGPU;
287 return true;
291 if (str.EqualsLiteral("bitmaprenderer")) {
292 *out_type = dom::CanvasContextType::ImageBitmap;
293 return true;
296 return false;
300 * This security check utility might be called from an source that never taints
301 * others. For example, while painting a CanvasPattern, which is created from an
302 * ImageBitmap, onto a canvas. In this case, the caller could set the CORSUsed
303 * true in order to pass this check and leave the aPrincipal to be a nullptr
304 * since the aPrincipal is not going to be used.
306 void DoDrawImageSecurityCheck(dom::HTMLCanvasElement* aCanvasElement,
307 nsIPrincipal* aPrincipal, bool forceWriteOnly,
308 bool CORSUsed) {
309 // Callers should ensure that mCanvasElement is non-null before calling this
310 if (!aCanvasElement) {
311 NS_WARNING("DoDrawImageSecurityCheck called without canvas element!");
312 return;
315 if (aCanvasElement->IsWriteOnly() && !aCanvasElement->mExpandedReader) {
316 return;
319 // If we explicitly set WriteOnly just do it and get out
320 if (forceWriteOnly) {
321 aCanvasElement->SetWriteOnly();
322 return;
325 // No need to do a security check if the image used CORS for the load
326 if (CORSUsed) return;
328 if (NS_WARN_IF(!aPrincipal)) {
329 MOZ_ASSERT_UNREACHABLE("Must have a principal here");
330 aCanvasElement->SetWriteOnly();
331 return;
334 if (aCanvasElement->NodePrincipal()->Subsumes(aPrincipal)) {
335 // This canvas has access to that image anyway
336 return;
339 if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
340 // This is a resource from an extension content script principal.
342 if (aCanvasElement->mExpandedReader &&
343 aCanvasElement->mExpandedReader->Subsumes(aPrincipal)) {
344 // This canvas already allows reading from this principal.
345 return;
348 if (!aCanvasElement->mExpandedReader) {
349 // Allow future reads from this same princial only.
350 aCanvasElement->SetWriteOnly(aPrincipal);
351 return;
354 // If we got here, this must be the *second* extension tainting
355 // the canvas. Fall through to mark it WriteOnly for everyone.
358 aCanvasElement->SetWriteOnly();
362 * This security check utility might be called from an source that never taints
363 * others. For example, while painting a CanvasPattern, which is created from an
364 * ImageBitmap, onto a canvas. In this case, the caller could set the aCORSUsed
365 * true in order to pass this check and leave the aPrincipal to be a nullptr
366 * since the aPrincipal is not going to be used.
368 void DoDrawImageSecurityCheck(dom::OffscreenCanvas* aOffscreenCanvas,
369 nsIPrincipal* aPrincipal, bool aForceWriteOnly,
370 bool aCORSUsed) {
371 // Callers should ensure that mCanvasElement is non-null before calling this
372 if (NS_WARN_IF(!aOffscreenCanvas)) {
373 return;
376 nsIPrincipal* expandedReader = aOffscreenCanvas->GetExpandedReader();
377 if (aOffscreenCanvas->IsWriteOnly() && !expandedReader) {
378 return;
381 // If we explicitly set WriteOnly just do it and get out
382 if (aForceWriteOnly) {
383 aOffscreenCanvas->SetWriteOnly();
384 return;
387 // No need to do a security check if the image used CORS for the load
388 if (aCORSUsed) {
389 return;
392 // If we are on a worker thread, we might not have any principals at all.
393 nsIGlobalObject* global = aOffscreenCanvas->GetOwnerGlobal();
394 nsIPrincipal* canvasPrincipal = global ? global->PrincipalOrNull() : nullptr;
395 if (!aPrincipal || !canvasPrincipal) {
396 aOffscreenCanvas->SetWriteOnly();
397 return;
400 if (canvasPrincipal->Subsumes(aPrincipal)) {
401 // This canvas has access to that image anyway
402 return;
405 if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
406 // This is a resource from an extension content script principal.
408 if (expandedReader && expandedReader->Subsumes(aPrincipal)) {
409 // This canvas already allows reading from this principal.
410 return;
413 if (!expandedReader) {
414 // Allow future reads from this same princial only.
415 aOffscreenCanvas->SetWriteOnly(aPrincipal);
416 return;
419 // If we got here, this must be the *second* extension tainting
420 // the canvas. Fall through to mark it WriteOnly for everyone.
423 aOffscreenCanvas->SetWriteOnly();
426 bool CoerceDouble(const JS::Value& v, double* d) {
427 if (v.isDouble()) {
428 *d = v.toDouble();
429 } else if (v.isInt32()) {
430 *d = double(v.toInt32());
431 } else if (v.isUndefined()) {
432 *d = 0.0;
433 } else {
434 return false;
436 return true;
439 bool HasDrawWindowPrivilege(JSContext* aCx, JSObject* /* unused */) {
440 return nsContentUtils::CallerHasPermission(aCx,
441 nsGkAtoms::all_urlsPermission);
444 bool CheckWriteOnlySecurity(bool aCORSUsed, nsIPrincipal* aPrincipal,
445 bool aHadCrossOriginRedirects) {
446 if (!aPrincipal) {
447 return true;
450 if (!aCORSUsed) {
451 if (aHadCrossOriginRedirects) {
452 return true;
455 nsIGlobalObject* incumbentSettingsObject = dom::GetIncumbentGlobal();
456 if (!incumbentSettingsObject) {
457 return true;
460 nsIPrincipal* principal = incumbentSettingsObject->PrincipalOrNull();
461 if (NS_WARN_IF(!principal) || !(principal->Subsumes(aPrincipal))) {
462 return true;
466 return false;
469 } // namespace mozilla::CanvasUtils