Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,6 @@ export let WebsiteFilter = {
contentType == Ci.nsIContentPolicy.TYPE_SUBDOCUMENT
) {
if (!this.isAllowed(url)) {
#ifdef MOZ_ENTERPRISE
this._recordBlocklistDomainBrowsed(url);
#endif
return Ci.nsIContentPolicy.REJECT_POLICY;
}
}
Expand All @@ -152,6 +149,16 @@ export let WebsiteFilter = {
url = URL.parse(location, channel.URI.spec);
}
if (url && !this.isAllowed(url.href)) {
#ifdef MOZ_ENTERPRISE
let referrerSpec = "";
try {
let referrerInfo = channel.referrerInfo;
if (referrerInfo) {
referrerSpec = referrerInfo.computedReferrerSpec;
}
} catch (e) {}
this._recordBlocklistDomainBrowsed(channel.originalURI.spec, url.href, referrerSpec);
#endif
channel.cancel(Cr.NS_ERROR_BLOCKED_BY_POLICY);
}
} catch (e) {}
Expand Down Expand Up @@ -180,7 +187,7 @@ export let WebsiteFilter = {
},
/* eslint-disable */
#ifdef MOZ_ENTERPRISE
_recordBlocklistDomainBrowsed(url) {
_recordBlocklistDomainBrowsed(originalUrl, resolvedUrl, referrer) {
const isEnabled = Services.prefs.getBoolPref(
"browser.policies.enterprise.telemetry.blocklistDomainBrowsed.enabled",
true
Expand All @@ -190,11 +197,23 @@ export let WebsiteFilter = {
}

try {
const processedUrl = this._processTelemetryUrl(url);
Glean.contentPolicy.blocklistDomainBrowsed.record({
url: processedUrl,
});
GleanPings.enterprise.submit();
const processedOrigUrl = this._processTelemetryUrl(originalUrl);
const processedResolvedUrl = this._processTelemetryUrl(resolvedUrl);
const processedReferrer = this._processTelemetryUrl(referrer);
const telemetryData = {
original_url: processedOrigUrl || "",
url: processedResolvedUrl || "",
referrer: processedReferrer || "",
};
Glean.contentPolicy.blocklistDomainBrowsed.record(telemetryData);
if (
!Services.prefs.getBoolPref(
"browser.policies.enterprise.telemetry.testing.disableSubmit",
false
)
) {
GleanPings.enterprise.submit();
}
} catch (ex) {
// Silently fail - telemetry errors should not break website filtering
console.error(
Expand Down
11 changes: 10 additions & 1 deletion browser/components/enterprisepolicies/metrics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ content_policy:
extra_keys:
url:
description: >
The blocked url that was browsed.
The url that was blocked by policy.
type: string
original_url:
description: >
The original url, prior to redirects, that was requested resulting in a block,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, for myself I set a block on morbo.be, then navigate to https://sjeng.org/referrer-hop.html, which is:

  document.getElementById("go").addEventListener("click", () => {
    location.href = "http://morbo.org/";
  });

morbo.org has a redirect to www.morbo.be

This records

  "events": [                                                                                                
    {                                                                                                        
      "category": "content_policy",                                                                          
      "extra": {                                                                                             
        "glean_timestamp": "1766755855522",                                                                  
        "original_url": "http://morbo.org/",                                                                 
        "referrer": "",                                                                                      
        "url": "http://www.morbo.be/"                                                                        
      },                                                                                                     
      "name": "blocklist_domain_browsed",                                                                    
      "timestamp": 0                                                                                         
    }                                                                                                        
  ],

a previous version of this patch was still getting the original referrer right:

  "events": [
    {
      "category": "content_policy",
      "extra": {
        "glean_timestamp": "1766060860297",
        "referrer": "https://sjeng.org/referrer-hop.html",
        "url": "http://www.morbo.be/"
      },
      "name": "blocklist_domain_browsed",
      "timestamp": 0
    }
  ],

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added https://sjeng.org/referrer-hop-direct.html, which is

  document.getElementById("go").addEventListener("click", () => {
    location.href = "http://www.morbo.be/";
  });

this records

  "events": [
    {
      "category": "content_policy",
      "extra": {
        "glean_timestamp": "1766756680769",
        "referrer": "",
        "url": "http://www.morbo.be/"
      },
      "name": "blocklist_domain_browsed",
      "timestamp": 0
    }
  ],

So the referrer isn't detected correctly (...any more).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an offline discussion we clarified that the lack of referrer in this case is a result of the choice to use the "computed referrer", i.e. matching what is in the "Referer" header after applying the referrer-policy. In these scenarios, the block is happening while executing an http request from an https referrer, so the policy has stripped the referrer.

It is technically easy to instead use the original referrer (pre-policy), so we're left with a judgment call of which is the correct data to include. I'll pursue that question and update here accordingly.

if different from the blocked url.
type: string
referrer:
description: >
The referrer address from which the url was requested.
type: string
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,10 @@ skip-if = [
["browser_policy_usermessaging.js"]

["browser_policy_websitefilter.js"]

["browser_policy_websitefilter_telemetry.js"]
# Only run in MOZ_ENTERPRISE builds
skip-if = [
"!buildapp:firefox",
"!enterprise",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";

const SUPPORT_FILES_PATH =
"http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser/";
const BLOCKED_PAGE = "policy_websitefilter_block.html";
const SAVELINKAS_PAGE = "policy_websitefilter_savelink.html";

async function clearWebsiteFilter() {
await setupPolicyEngineWithJson({
policies: {
WebsiteFilter: {
Block: [],
Exceptions: [],
},
},
});
}

add_task(async function test_policy_enterprise_telemetry() {
await setupPolicyEngineWithJson({
policies: {
WebsiteFilter: `{
"Block": ["*://mochi.test/*policy_websitefilter_block*"]
}`,
},
});
await SpecialPowers.pushPrefEnv({
set: [
["browser.policies.enterprise.telemetry.testing.disableSubmit", true],
[
"browser.policies.enterprise.telemetry.blocklistDomainBrowsed.enabled",
true,
],
[
"browser.policies.enterprise.telemetry.blocklistDomainBrowsed.urlLogging",
"full",
],
],
});

const referrerURL = SUPPORT_FILES_PATH + SAVELINKAS_PAGE;
const resolvedURL = SUPPORT_FILES_PATH + BLOCKED_PAGE;
await checkBlockedPageTelemetry(SUPPORT_FILES_PATH + BLOCKED_PAGE);
await checkBlockedPageTelemetry(SUPPORT_FILES_PATH + BLOCKED_PAGE, {
referrerURL,
});
await checkBlockedPageTelemetry(
"view-source:" + SUPPORT_FILES_PATH + BLOCKED_PAGE
);
await checkBlockedPageTelemetry(
"about:reader?url=" + SUPPORT_FILES_PATH + BLOCKED_PAGE
);

await checkBlockedPageTelemetry(SUPPORT_FILES_PATH + "301.sjs", {
resolvedURL,
});
await checkBlockedPageTelemetry(SUPPORT_FILES_PATH + "301.sjs", {
resolvedURL,
referrerURL,
});

await checkBlockedPageTelemetry(SUPPORT_FILES_PATH + "302.sjs", {
resolvedURL,
});
await checkBlockedPageTelemetry(SUPPORT_FILES_PATH + "302.sjs", {
resolvedURL,
referrerURL,
});

await clearWebsiteFilter();
});

// Checks that a page was blocked by seeing if it was replaced with about:neterror
async function checkBlockedPageTelemetry(
url,
{ resolvedURL, referrerURL } = {}
) {
const expectedBlockedUrl = resolvedURL ?? url;

let newTab;
try {
if (referrerURL) {
newTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
referrerURL
);

await SpecialPowers.spawn(newTab.linkedBrowser, [url], async href => {
let link = content.document.getElementById("savelink_blocked");
link.href = href;
});
} else {
newTab = BrowserTestUtils.addTab(gBrowser);
gBrowser.selectedTab = newTab;
}
let browser = newTab.linkedBrowser;

let promise = BrowserTestUtils.waitForErrorPage(browser);
if (referrerURL) {
await BrowserTestUtils.synthesizeMouseAtCenter(
"#savelink_blocked",
{},
browser
);
} else {
BrowserTestUtils.startLoadingURIString(browser, url);
}
await promise;

let events =
Glean.contentPolicy.blocklistDomainBrowsed.testGetValue("enterprise");
Assert.ok(events?.length, "Should have recorded events");
if (!events?.length) {
return;
}
Assert.greaterOrEqual(events.length, 1, "Should record at least one event"); // TODO this should eventually be exactly 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this refers to the debouncing/deduplication which this is still missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I planned to implement the debouncing/deduplication in a separate PR. I don't know if we have a policy around landing TODOs into the code base and/or tagging them with links to bugs to track, etc.?

const event = events.at(-1);
Assert.ok(event.extra, "Event should have extra data");
Assert.equal(
event.extra.url,
expectedBlockedUrl,
"Telemetry should include blocked URL"
);
if (resolvedURL) {
Assert.equal(
event.extra.original_url,
url,
"Telemetry should include original requested URL"
);
}
if (referrerURL) {
Assert.equal(
event.extra.referrer,
referrerURL,
"Telemetry should include referrer URL"
);
}
} finally {
if (newTab) {
BrowserTestUtils.removeTab(newTab);
}
Services.fog.testResetFOG();
}
}
109 changes: 109 additions & 0 deletions dom/security/nsContentSecurityManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
#include "mozilla/dom/nsMixedContentBlocker.h"
#include "mozilla/extensions/WebExtensionPolicy.h"
#include "mozilla/glean/DomSecurityMetrics.h"
#if defined(MOZ_ENTERPRISE)
# include "mozilla/glean/EnterprisepoliciesMetrics.h"
# include "mozilla/glean/GleanPings.h"
#endif
#include "nsAboutProtocolUtils.h"
#include "nsArray.h"
#include "nsCORSListenerProxy.h"
Expand Down Expand Up @@ -64,6 +68,108 @@ NS_IMPL_ISUPPORTS(nsContentSecurityManager, nsIContentSecurityManager,
mozilla::LazyLogModule sCSMLog("CSMLog");
mozilla::LazyLogModule sUELLog("UnexpectedLoad");

#if defined(MOZ_ENTERPRISE)

static bool BlocklistDomainBrowsedTelemetryIsEnabled() {
return Preferences::GetBool(
"browser.policies.enterprise.telemetry.blocklistDomainBrowsed.enabled",
true);
}

static nsCString BlocklistDomainBrowsedTelemetryUrlLoggingPolicy() {
nsCString urlLogging;
if (NS_FAILED(Preferences::GetCString("browser.policies.enterprise.telemetry."
"blocklistDomainBrowsed.urlLogging",
urlLogging)) ||
urlLogging.IsEmpty()) {
urlLogging.AssignLiteral("full");
}
return urlLogging;
}

static nsCString ProcessBlocklistDomainBrowsedTelemetryUrl(
nsIURI* aURI, const nsCString& aPolicy) {
if (!aURI || aPolicy.EqualsLiteral("none")) {
return ""_ns;
}

if (aPolicy.EqualsLiteral("domain")) {
nsCString host;
if (NS_FAILED(aURI->GetHost(host))) {
return ""_ns;
}
return host;
}

nsCString spec;
aURI->GetSpec(spec);
return spec;
}

static nsCString ProcessBlocklistDomainBrowsedTelemetryUrlSpec(
const nsCString& aSpec, const nsCString& aPolicy) {
if (aPolicy.EqualsLiteral("none")) {
return ""_ns;
}

if (!aPolicy.EqualsLiteral("domain")) {
return aSpec;
}

nsCOMPtr<nsIURI> parsed;
if (NS_FAILED(NS_NewURI(getter_AddRefs(parsed), aSpec)) || !parsed) {
return ""_ns;
}
return ProcessBlocklistDomainBrowsedTelemetryUrl(parsed, aPolicy);
}

static nsCString GetBlocklistDomainBrowsedReferrerSpec(nsIChannel* aChannel) {
nsCString referrerSpec;
nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(aChannel);
if (!httpChan) {
return referrerSpec;
}

nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChan->GetReferrerInfo();
if (!referrerInfo) {
return referrerSpec;
}

referrerInfo->GetComputedReferrerSpec(referrerSpec);
return referrerSpec;
}

static void RecordBlocklistDomainBrowsedTelemetry(nsIChannel* aChannel,
nsIURI* aURI) {
if (!BlocklistDomainBrowsedTelemetryIsEnabled()) {
return;
}

const nsCString urlLogging =
BlocklistDomainBrowsedTelemetryUrlLoggingPolicy();

const nsCString blockedUrlTelemetry =
ProcessBlocklistDomainBrowsedTelemetryUrl(aURI, urlLogging);

const nsCString referrerSpec =
GetBlocklistDomainBrowsedReferrerSpec(aChannel);
const nsCString referrerTelemetry =
ProcessBlocklistDomainBrowsedTelemetryUrlSpec(referrerSpec, urlLogging);

glean::content_policy::BlocklistDomainBrowsedExtra extra = {
.referrer = Some(referrerTelemetry),
.url = Some(blockedUrlTelemetry),
};
glean::content_policy::blocklist_domain_browsed.Record(Some(extra));

if (!Preferences::GetBool(
"browser.policies.enterprise.telemetry.testing.disableSubmit",
false)) {
glean_pings::Enterprise.Submit();
}
}
#endif

// These first two are used for off-the-main-thread checks of
// general.config.filename
// (which can't be checked off-main-thread).
Expand Down Expand Up @@ -537,6 +643,9 @@ static nsresult DoContentSecurityChecks(nsIChannel* aChannel,
return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT;
}
if (shouldLoad == nsIContentPolicy::REJECT_POLICY) {
#if defined(MOZ_ENTERPRISE)
RecordBlocklistDomainBrowsedTelemetry(aChannel, uri);
#endif
return NS_ERROR_BLOCKED_BY_POLICY;
}
if (shouldLoad == nsIContentPolicy::REJECT_RESTARTFORCED) {
Expand Down
Loading