Bug 1914203 - Add telemetry for notification permission r=asuth,bvandersloot

Following the parent patch in bug 1914417, this will allow us to decide whether we can block ABA access or not. (Bonus, also whether we can remove requestPermission in non-secure context.)

Differential Revision: https://phabricator.services.mozilla.com/D219777
This commit is contained in:
Kagami Sascha Rosylight 2024-09-20 14:02:28 +00:00
parent b710b85738
commit 183d09046b
9 changed files with 287 additions and 6 deletions

View File

@ -10,10 +10,38 @@
#include "mozilla/Components.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/NotificationBinding.h"
#include "mozilla/glean/GleanMetrics.h"
#include "nsIPermissionManager.h"
namespace mozilla::dom::notification {
using GleanLabel = glean::web_notification::ShowOriginLabel;
static void ReportTelemetry(GleanLabel aLabel,
PermissionCheckPurpose aPurpose) {
switch (aPurpose) {
case PermissionCheckPurpose::PermissionAttribute:
glean::web_notification::permission_origin
.EnumGet(static_cast<glean::web_notification::PermissionOriginLabel>(
aLabel))
.Add();
return;
case PermissionCheckPurpose::PermissionRequest:
glean::web_notification::request_permission_origin
.EnumGet(static_cast<
glean::web_notification::RequestPermissionOriginLabel>(
aLabel))
.Add();
return;
case PermissionCheckPurpose::NotificationShow:
glean::web_notification::show_origin.EnumGet(aLabel).Add();
return;
default:
MOZ_CRASH("Unknown permission checker");
return;
}
}
bool IsNotificationAllowedFor(nsIPrincipal* aPrincipal) {
if (aPrincipal->IsSystemPrincipal()) {
return true;
@ -34,6 +62,7 @@ bool IsNotificationForbiddenFor(nsIPrincipal* aPrincipal,
if (!isSecureContext) {
if (aRequestorDoc) {
glean::web_notification::insecure_context_permission_request.Add();
nsContentUtils::ReportToConsole(
nsIScriptError::errorFlag, "DOM"_ns, aRequestorDoc,
nsContentUtils::eDOM_PROPERTIES,
@ -48,6 +77,7 @@ bool IsNotificationForbiddenFor(nsIPrincipal* aPrincipal,
if (aEffectiveStoragePrincipal->OriginAttributesRef()
.mPartitionKey.IsEmpty()) {
// first party
ReportTelemetry(GleanLabel::eFirstParty, aPurpose);
return false;
}
nsString outScheme;
@ -58,10 +88,12 @@ bool IsNotificationForbiddenFor(nsIPrincipal* aPrincipal,
outPort, outForeignByAncestorContext);
if (outForeignByAncestorContext) {
// nested first party
ReportTelemetry(GleanLabel::eNestedFirstParty, aPurpose);
return false;
}
// third party
ReportTelemetry(GleanLabel::eThirdParty, aPurpose);
if (aRequestorDoc) {
nsContentUtils::ReportToConsole(
nsIScriptError::errorFlag, "DOM"_ns, aRequestorDoc,

View File

@ -0,0 +1,69 @@
# 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/.
# Adding a new metric? We have docs for that!
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
- 'Core :: DOM: Notifications'
web_notification:
insecure_context_permission_request:
type: counter
description: >
Whether we saw a permission request from an insecure context.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
notification_emails:
- krosylight@mozilla.com
expires: never
show_origin:
type: labeled_counter
description: >
The category of the origin that calls new Notification/showNotification().
labels:
- first_party
- third_party
- nested_first_party
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
notification_emails:
- krosylight@mozilla.com
expires: never
permission_origin:
type: labeled_counter
description: >
The category of the origin that retrieves Notification.permission.
labels:
- first_party
- third_party
- nested_first_party
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
notification_emails:
- krosylight@mozilla.com
expires: never
request_permission_origin:
type: labeled_counter
description: >
The category of the origin that calls Notification.requestPermission().
labels:
- first_party
- third_party
- nested_first_party
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1914203
notification_emails:
- krosylight@mozilla.com
expires: never

View File

@ -0,0 +1,53 @@
/* 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/. */
"use strict";
// Bug 1799977: Using workaround to test telemetry in plain mochitests
const GleanTest = new Proxy(
{
async testResetFOG() {
return SpecialPowers.spawnChrome([], async () => {
await Services.fog.testFlushAllChildren();
Services.fog.testResetFOG();
});
},
},
{
get(gleanTestObj, gleanTestProp) {
if (gleanTestProp in gleanTestObj) {
return gleanTestObj[gleanTestProp];
}
return new Proxy(
{},
{
get(categoryObj, categoryProp) {
return new Proxy(
{},
{
get(metricObj, metricProp) {
return {
async testGetValue() {
return SpecialPowers.spawnChrome(
[gleanTestProp, categoryProp, metricProp],
async (categoryName, metricName, label) => {
await Services.fog.testFlushAllChildren();
const window = this.browsingContext.topChromeWindow;
return window.Glean[categoryName][metricName][
label
].testGetValue();
}
);
},
};
},
}
);
},
}
);
},
}
);

View File

@ -10,7 +10,7 @@ var NotificationTest = (function () {
// it can be used to track data between tests
var context = {};
(function executeRemainingTests(remainingTests) {
(async function executeRemainingTests(remainingTests) {
if (!remainingTests.length) {
callback();
return;
@ -21,14 +21,14 @@ var NotificationTest = (function () {
var startTest = nextTest.call.bind(nextTest, context, finishTest);
try {
startTest();
await startTest();
// if no callback was defined for test function,
// we must manually invoke finish to continue
if (nextTest.length === 0) {
finishTest();
}
} catch (e) {
ok(false, "Test threw exception!");
ok(false, `Test threw exception: ${e}`);
finishTest();
}
})(tests);

View File

@ -3,6 +3,7 @@ scheme = "https"
support-files = [
"MockAlertsService.js",
"NotificationTest.js",
"GleanTest.js",
]
["test_notification_basics.html"]
@ -14,6 +15,9 @@ skip-if = [
["test_notification_crossorigin_iframe.html"]
support-files = ["blank.html"]
["test_notification_crossorigin_iframe_nested_glean.html"]
support-files = ["blank.html"]
# This test needs to be run on HTTP (not HTTPS).
["test_notification_insecure_context.html"]
skip-if = [

View File

@ -3,14 +3,15 @@
<head>
<title>Notification Basics</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="NotificationTest.js"></script>
<script src="NotificationTest.js"></script>
<script src="GleanTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<script type="text/javascript">
<script>
var info = NotificationTest.info;
var options;
@ -18,7 +19,7 @@
SimpleTest.requestFlakyTimeout("untriaged");
var steps = [
function() {
async function() {
info("Test notification spec");
ok(Notification, "Notification constructor exists");
ok(Notification.permission, "Notification.permission exists");
@ -30,6 +31,23 @@
Notification.requestPermission();
},
async function() {
info("Test Glean telemetry");
await GleanTest.testResetFOG();
await Notification.requestPermission();
const requestCount = await GleanTest.webNotification.requestPermissionOrigin.first_party.testGetValue();
is(requestCount, 1, "Notification first party request permission counter should increment once.");
Notification.permission;
const permissionCount = await GleanTest.webNotification.permissionOrigin.first_party.testGetValue();
is(permissionCount, 1, "Notification first party request permission counter should increment once.");
await new Promise(r => new Notification("first party").onerror = r);
const showCount = await GleanTest.webNotification.showOrigin.first_party.testGetValue();
is(showCount, 1, "Notification first party request permission counter should increment once.");
},
async function(done) {
info("Test requestPermission deny");
function assertPermissionDenied(perm) {

View File

@ -7,6 +7,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1560741
<head>
<title>Notification permission in cross-origin iframes</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="GleanTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
@ -37,6 +38,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1560741
]);
});
await GleanTest.testResetFOG();
let checkRequest = async (expectedResponse, msg) => {
let response = await this.content.Notification.requestPermission();
Assert.equal(response, expectedResponse, msg);
@ -46,6 +49,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1560741
["denied", "Denied permission in cross-origin iframe"],
checkRequest);
const requestCount = await GleanTest.webNotification.requestPermissionOrigin.third_party.testGetValue();
is(requestCount, 1, "Notification third party request permission counter should increment once.");
let checkPermission = async (expectedPermission, msg) => {
let permission = this.content.Notification.permission;
Assert.equal(permission, expectedPermission, msg);
@ -55,6 +61,25 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1560741
["denied", "Permission is denied in cross-origin iframe"],
checkPermission);
const permissionCount = await GleanTest.webNotification.permissionOrigin.third_party.testGetValue();
is(permissionCount, 1, "Notification third party permission read counter should increment once.");
let checkConstruct = async (expectedShown, msg) => {
const shown = await new Promise(r => {
const n = new this.content.Notification("cross origin");
n.onshow = () => r(true);
n.onerror = () => r(false);
});
Assert.equal(shown, expectedShown, msg);
};
await SpecialPowers.spawn(iframe,
[false, "Notification constructor should error in cross-origin iframe"],
checkConstruct);
const showCount = await GleanTest.webNotification.showOrigin.third_party.testGetValue();
is(showCount, 1, "Notification third party show attempt counter should increment once.");
await SpecialPowers.pushPrefEnv({"set": [["dom.webnotifications.allowcrossoriginiframe", true]]});
await SpecialPowers.spawn(iframe,

View File

@ -0,0 +1,79 @@
<!DOCTYPE HTML>
<html>
<!--
Tests that Notification permissions are denied in cross-origin iframes.
https://bugzilla.mozilla.org/show_bug.cgi?id=1560741
-->
<head>
<title>Notification permission in cross-origin iframes</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="GleanTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
const kBlankPath = "/tests/dom/notification/test/mochitest/blank.html"
const kParentURL = "https://example.org" + kBlankPath;
const kChildURL = "https://example.com" + kBlankPath;
(async function runTest() {
let iframe = document.createElement("iframe");
iframe.src = kParentURL;
document.body.appendChild(iframe);
await new Promise(resolve => {
iframe.onload = resolve;
});
await SpecialPowers.spawn(iframe, [kChildURL], async (childURL) => {
const nested = this.content.document.createElement("iframe");
nested.src = childURL;
this.content.document.body.appendChild(nested);
await new Promise(resolve => {
nested.onload = resolve;
});
});
await GleanTest.testResetFOG();
await SpecialPowers.spawn(iframe, [], async () => {
const nested = this.content.document.querySelector("iframe");
await SpecialPowers.spawn(nested, [], async () => {
await this.content.Notification.requestPermission();
});
});
const requestCount = await GleanTest.webNotification.requestPermissionOrigin.nested_first_party.testGetValue();
is(requestCount, 1, "Notification third party request permission counter should increment once.");
await SpecialPowers.spawn(iframe, [], async () => {
const nested = this.content.document.querySelector("iframe");
await SpecialPowers.spawn(nested, [], async () => {
this.content.Notification.permission;
});
});
const permissionCount = await GleanTest.webNotification.permissionOrigin.nested_first_party.testGetValue();
is(permissionCount, 1, "Notification third party permission read counter should increment once.");
await SpecialPowers.spawn(iframe, [], async () => {
const nested = this.content.document.querySelector("iframe");
await SpecialPowers.spawn(nested, [], async () => {
new this.content.Notification("cross origin");
});
});
const showCount = await GleanTest.webNotification.showOrigin.nested_first_party.testGetValue();
is(showCount, 1, "Notification third party show attempt counter should increment once.");
SimpleTest.finish();
})();
</script>
</pre>
</body>
</html>

View File

@ -24,6 +24,7 @@ gecko_metrics = [
"dom/media/metrics.yaml",
"dom/media/webrtc/metrics.yaml",
"dom/metrics.yaml",
"dom/notification/metrics.yaml",
"dom/performance/metrics.yaml",
"dom/security/metrics.yaml",
"dom/webauthn/metrics.yaml",