Skip to content

Commit b87087b

Browse files
author
Fatih
committed
Bug 1422862: Show canvas permission prompt for offscreen canvases. r=tjr,gfx-reviewers,nical
This patch implements IsImageExtractionAllowed for Offscreen Canvas. It gets the windowId from offscreen canvas, queries it, attempts to get window's document, shows canvas permission prompt if successful. Differential Revision: https://phabricator.services.mozilla.com/D222687
1 parent ad6b0a2 commit b87087b

File tree

1 file changed

+206
-9
lines changed

1 file changed

+206
-9
lines changed

dom/canvas/CanvasUtils.cpp

Lines changed: 206 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "mozilla/dom/OffscreenCanvas.h"
1515
#include "mozilla/dom/UserActivation.h"
1616
#include "mozilla/dom/WorkerCommon.h"
17+
#include "mozilla/dom/WindowGlobalParent.h"
1718
#include "mozilla/dom/WorkerPrivate.h"
1819
#include "mozilla/dom/WorkerRunnable.h"
1920
#include "mozilla/gfx/gfxVars.h"
@@ -101,12 +102,12 @@ uint32_t GetCanvasExtractDataPermission(nsIPrincipal& aPrincipal) {
101102
nsresult rv;
102103
nsCOMPtr<nsIPermissionManager> permissionManager =
103104
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
104-
NS_ENSURE_SUCCESS(rv, false);
105+
NS_ENSURE_SUCCESS(rv, nsIPermissionManager::UNKNOWN_ACTION);
105106

106107
uint32_t permission;
107108
rv = permissionManager->TestPermissionFromPrincipal(
108109
&aPrincipal, PERMISSION_CANVAS_EXTRACT_DATA, &permission);
109-
NS_ENSURE_SUCCESS(rv, false);
110+
NS_ENSURE_SUCCESS(rv, nsIPermissionManager::UNKNOWN_ACTION);
110111

111112
return permission;
112113
}
@@ -331,20 +332,216 @@ ImageExtraction ImageExtractionResult(dom::HTMLCanvasElement* aCanvasElement,
331332
return ImageExtraction::Unrestricted;
332333
}
333334

335+
/*
336+
┌──────────────────────────────────────────────────────────────────────────┐
337+
│IsImageExtractionAllowed(dom::OffscreenCanvas*, JSContext*, nsIPrincipal&)│
338+
└────────────────────────────────────┬─────────────────────────────────────┘
339+
340+
┌─────────────────▼────────────────────┐
341+
┌─────No──────────│Any prompt RFP target enabled? See [1]│
342+
▼ └─────────────────┬────────────────────┘
343+
│ │Yes
344+
│ ┌─────────────────▼────────┐
345+
├─────Yes─────────┤Is unrestricted principal?│
346+
▼ └─────────────────┬────────┘
347+
│ │No
348+
│ ┌─────────────────▼────────┐
349+
│ ┌──No──┤Are third parties blocked?│
350+
│ │ └─────────────────┬────────┘
351+
│ │ │Yes
352+
│ │ ┌─────────────────▼─────────────┐
353+
│ │ │Are we in a third-party window?├───────Yes──────────┐
354+
│ │ └─────────────────┬─────────────┘ ▼
355+
│ │ │No │
356+
│ │ ┌─────────────────▼──┐ │
357+
│ └──────►Do we show a prompt?├────────────Yes─┐ │
358+
│ └─────────────────┬──┘ ▼ │
359+
│ │No │ │
360+
│ ┌─────────────────▼─────────────┐ │ │
361+
│ │Do we allow reading canvas data│ │ │
362+
│ │in response to user input? ├─No──┤ │
363+
│ └─────────────────┬─────────────┘ ▼ │
364+
│ │Yes │ │
365+
│ ┌─────────────────▼─────────┐ │ │
366+
├─────Yes─────────┼Are we handling user input?│ │ │
367+
▼ └─────────────────┬─────────┘ │ │
368+
│ │No │ │
369+
│ ┌─────────────────▼─────────────┐ │ │
370+
┌▼─────┐ │Show Permission Prompt (either ◄─────┘ ┌───▼──┐
371+
│return│ │w/ doorhanger, or w/o depending│ │return│
372+
│true │ │on User Input) ├────────────────►false │
373+
└──────┘ └───────────────────────────────┘ └──────┘
374+
[1]: CanvasImageExtractionPrompt, CanvasExtractionBeforeUserInputIsBlocked,
375+
CanvasExtractionFromThirdPartiesIsBlocked are the RFP targets mentioned.
376+
*/
377+
bool IsImageExtractionAllowed(dom::OffscreenCanvas* aOffscreenCanvas,
378+
JSContext* aCx, nsIPrincipal& aPrincipal) {
379+
if (!aOffscreenCanvas) {
380+
return false;
381+
}
382+
383+
bool canvasImageExtractionPrompt =
384+
aOffscreenCanvas->ShouldResistFingerprinting(
385+
RFPTarget::CanvasImageExtractionPrompt);
386+
bool canvasExtractionBeforeUserInputIsBlocked =
387+
aOffscreenCanvas->ShouldResistFingerprinting(
388+
RFPTarget::CanvasExtractionBeforeUserInputIsBlocked);
389+
bool canvasExtractionFromThirdPartiesIsBlocked =
390+
aOffscreenCanvas->ShouldResistFingerprinting(
391+
RFPTarget::CanvasExtractionFromThirdPartiesIsBlocked);
392+
393+
if (!canvasImageExtractionPrompt &&
394+
!canvasExtractionBeforeUserInputIsBlocked &&
395+
!canvasExtractionFromThirdPartiesIsBlocked) {
396+
return true;
397+
}
398+
399+
// Don't proceed if we don't have a document or JavaScript context.
400+
if (!aCx) {
401+
return false;
402+
}
403+
404+
if (IsUnrestrictedPrincipal(aPrincipal)) {
405+
return true;
406+
}
407+
408+
// Define some lambdas to help with the logic
409+
410+
Maybe<uint64_t> winId = Nothing();
411+
RefPtr<dom::WindowContext> win = nullptr;
412+
// Tries to get window Id and window. We need window
413+
// multiple times or zero times depending on the conditions.
414+
// So, we "cache" the window Id and window.
415+
auto tryWin = [&]() {
416+
if (win) {
417+
return win;
418+
}
419+
420+
if (winId.isNothing()) {
421+
winId = aOffscreenCanvas->GetWindowID();
422+
if (winId.isNothing()) {
423+
// Do not try again if we failed to get the window ID
424+
winId = Some(UINT64_MAX);
425+
}
426+
} else {
427+
// Workers created outside of a window will return UINT64_MAX
428+
if (*winId == UINT64_MAX) {
429+
return win;
430+
}
431+
}
432+
433+
win = dom::WindowGlobalParent::GetById(*winId);
434+
return win;
435+
};
436+
437+
Maybe<nsAutoCString> origin = Nothing();
438+
auto tryOrigin = [&]() {
439+
if (origin.isSome()) {
440+
return !origin->IsEmpty();
441+
}
442+
443+
nsAutoCString originResult;
444+
nsresult rv = aPrincipal.GetOrigin(originResult);
445+
origin = NS_FAILED(rv) ? Some(""_ns) : Some(originResult);
446+
return NS_SUCCEEDED(rv);
447+
};
448+
449+
auto reportToConsole = [&](nsLiteralCString reason) {
450+
if (!tryWin()) {
451+
return;
452+
}
453+
454+
nsAutoString message;
455+
message.Append(u"Blocked ");
456+
if (tryOrigin()) {
457+
message.AppendPrintf("%s's ", origin->get());
458+
}
459+
message.AppendPrintf("offscreen canvas from extracting canvas data %s.",
460+
reason.get());
461+
nsContentUtils::ReportToConsoleByWindowID(
462+
message, nsIScriptError::warningFlag, "Security"_ns, *winId);
463+
};
464+
465+
// Logic continues from here
466+
467+
if (canvasExtractionFromThirdPartiesIsBlocked) {
468+
if (!tryWin() || win->GetIsThirdPartyWindow()) {
469+
reportToConsole(
470+
"because third party window tried to extract canvas data"_ns);
471+
return false;
472+
}
473+
}
474+
475+
if (!canvasImageExtractionPrompt &&
476+
!canvasExtractionBeforeUserInputIsBlocked) {
477+
return true;
478+
}
479+
480+
// -------------------------------------------------------------------
481+
// At this point, there's only one way to return true: if we are always
482+
// allowing canvas in response to user input, and not prompting
483+
bool hidePermissionDoorhanger = false;
484+
if (!canvasImageExtractionPrompt &&
485+
canvasExtractionBeforeUserInputIsBlocked) {
486+
// If so, see if this is in response to user input.
487+
if (dom::UserActivation::IsHandlingUserInput()) {
488+
return true;
489+
}
490+
491+
hidePermissionDoorhanger = true;
492+
}
493+
494+
// -------------------------------------------------------------------
495+
// Now we know we're going to block it, and log something to the console,
496+
// and show some sort of prompt maybe with the doorhanger, maybe not
497+
498+
hidePermissionDoorhanger |= canvasExtractionBeforeUserInputIsBlocked &&
499+
!dom::UserActivation::IsHandlingUserInput();
500+
501+
reportToConsole(hidePermissionDoorhanger
502+
? "because no user input was detected"_ns
503+
: "but prompting the user."_ns);
504+
505+
// Show the prompt to the user (asynchronous) - maybe with the doorhanger,
506+
// maybe not
507+
if (!tryOrigin() || !tryWin()) {
508+
return false;
509+
}
510+
511+
NS_DispatchToMainThread(
512+
NS_NewRunnableFunction("IsImageExtractionAllowedOffscreen", [=]() {
513+
if (XRE_IsContentProcess()) {
514+
dom::BrowserChild* browserChild =
515+
dom::BrowserChild::GetFrom(win->GetExtantDoc()->GetWindow());
516+
517+
if (browserChild) {
518+
browserChild->SendShowCanvasPermissionPrompt(
519+
*origin, hidePermissionDoorhanger);
520+
}
521+
} else {
522+
nsCOMPtr<nsIObserverService> obs =
523+
mozilla::services::GetObserverService();
524+
if (obs) {
525+
obs->NotifyObservers(
526+
win,
527+
hidePermissionDoorhanger
528+
? TOPIC_CANVAS_PERMISSIONS_PROMPT_HIDE_DOORHANGER
529+
: TOPIC_CANVAS_PERMISSIONS_PROMPT,
530+
NS_ConvertUTF8toUTF16(*origin).get());
531+
}
532+
}
533+
}));
534+
return false;
535+
}
536+
334537
ImageExtraction ImageExtractionResult(dom::OffscreenCanvas* aOffscreenCanvas,
335538
JSContext* aCx,
336539
nsIPrincipal& aPrincipal) {
337540
if (IsUnrestrictedPrincipal(aPrincipal)) {
338541
return ImageExtraction::Unrestricted;
339542
}
340543

341-
if (aOffscreenCanvas->ShouldResistFingerprinting(
342-
RFPTarget::CanvasImageExtractionPrompt)) {
343-
if (GetCanvasExtractDataPermission(aPrincipal) ==
344-
nsIPermissionManager::ALLOW_ACTION) {
345-
return ImageExtraction::Unrestricted;
346-
}
347-
544+
if (!IsImageExtractionAllowed(aOffscreenCanvas, aCx, aPrincipal)) {
348545
return ImageExtraction::Placeholder;
349546
}
350547

0 commit comments

Comments
 (0)