Skip to content

Commit

Permalink
Re-enable ServiceWorker initiated request blocking
Browse files Browse the repository at this point in the history
A bug in the chrome.declarativeNetRequest API[1] meant that extensions could
block requests initiated by other extensions. This cross-extension request
blocking wasn't supposed to be possible[2], and led to lots of issues for users
with multiple browser extensions installed (for example, see this Privacy Badger
bug report[3]).

These cross-extension requests weren't associated with a tabId, and so could not
be differentiated from other such requests - including ServiceWorker initiated
requests. As a workaround, to avoid breaking the other extensions our users
might have installed, we stopped blocking any requests with no associated tabId.

Since then, we have fixed the bug in Chrome[4], and that fix was release with
Chrome 128. So let's re-enable ServiceWorker initiated request blocking again
and increase the minimum supported Chrome version to 128.

1 - https://crbug.com/40896400
2 - w3c/webextensions#369
3 - EFForg/privacybadger#2968
4 - https://chromiumdash.appspot.com/commit/486d638e6977dad73bd207dc914df9319afac152
  • Loading branch information
kzar committed Oct 30, 2024
1 parent 6e55438 commit 7dca719
Show file tree
Hide file tree
Showing 7 changed files with 3 additions and 185 deletions.
2 changes: 1 addition & 1 deletion browsers/chrome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "__MSG_appDesc__",
"default_locale": "en",
"version": "2024.10.16",
"minimum_chrome_version": "121.0",
"minimum_chrome_version": "128.0",
"icons": {
"16": "img/icon_16.png",
"48": "img/icon_48.png",
Expand Down
46 changes: 0 additions & 46 deletions integration-test/request-blocking.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,47 +105,6 @@ test.describe('Test request blocking', () => {
await page.close()
})

test('serviceworkerInitiatedRequests exceptions should disable service worker blocking', async ({ page, backgroundPage, context, backgroundNetworkContext, manifestVersion }) => {
await overridePrivacyConfig(backgroundNetworkContext, 'serviceworker-blocking.json')
await forExtensionLoaded(context)
await forAllConfiguration(backgroundPage)
if (manifestVersion === 3) {
await forDynamicDNRRulesLoaded(backgroundPage)
}
await backgroundPage.evaluate(async (domain) => {
/** @type {import('../shared/js/background/components/resource-loader').default} */
const configLoader = globalThis.components.tds.config
await configLoader.modify((config) => {
config.features.serviceworkerInitiatedRequests.exceptions.push({
domain,
reason: 'test'
})
return config
})
}, testHost)
const [, pageRequests] = await runRequestBlockingTest(page)

// Verify that no logged requests were allowed.
for (const { url, method, type, status } of pageRequests) {
const description = `URL: ${url}, Method: ${method}, Type: ${type}`
expect(status, description).toEqual('blocked')
}

// Check that the test page itself agrees that no requests were
// allowed.
const pageResults = await page.evaluate(
() => results.results // eslint-disable-line no-undef
)
for (const { id, category, status } of pageResults) {
const description = `ID: ${id}, Category: ${category}`
if (id === 'serviceworker-fetch') {
expect(status, description).toEqual('loaded')
} else {
expect(status, description).not.toEqual('loaded')
}
}
})

test('Blocking should not run on localhost', async ({ page, backgroundPage, context, manifestVersion, backgroundNetworkContext }) => {
await overridePrivacyConfig(backgroundNetworkContext, 'serviceworker-blocking.json')
await forExtensionLoaded(context)
Expand Down Expand Up @@ -213,11 +172,6 @@ test.describe('Test request blocking', () => {
if (['video', 'websocket'].includes(id)) {
continue
}
// serviceworker-fetch: allowlist does not work in MV3
// https://app.asana.com/0/892838074342800/1204515863331825/f
if (manifestVersion === 3 && id === 'serviceworker-fetch') {
continue
}
const description = `ID: ${id}, Category: ${category}`
expect(status, description).toEqual('loaded')
}
Expand Down
1 change: 0 additions & 1 deletion packages/ddg2dnr/lib/rulePriorities.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// there is not a better place to put them.

exports.AD_ATTRIBUTION_POLICY_PRIORITY = 30000
exports.SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY = 1000000
exports.USER_ALLOWLISTED_PRIORITY = 1000000
exports.ATB_PARAM_PRIORITY = 2000000
exports.NEWTAB_TRACKER_STATS_REDIRECT_PRIORITY = 2000000
14 changes: 2 additions & 12 deletions packages/ddg2dnr/test/rulePriorities.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const {

const {
AD_ATTRIBUTION_POLICY_PRIORITY,
SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY,
USER_ALLOWLISTED_PRIORITY,
ATB_PARAM_PRIORITY,
NEWTAB_TRACKER_STATS_REDIRECT_PRIORITY
Expand All @@ -57,7 +56,6 @@ describe('Rule Priorities', () => {
assert.equal(TRACKING_PARAM_PRIORITY, 40000)
assert.equal(COOKIE_PRIORITY, 40000)
assert.equal(UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY, 1000000)
assert.equal(SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY, 1000000)
assert.equal(USER_ALLOWLISTED_PRIORITY, 1000000)
assert.equal(ATB_PARAM_PRIORITY, 2000000)
assert.equal(NEWTAB_TRACKER_STATS_REDIRECT_PRIORITY, 2000000)
Expand All @@ -67,12 +65,10 @@ describe('Rule Priorities', () => {
// ATB is higher than allowlist
assert.ok(ATB_PARAM_PRIORITY > UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY)
assert.ok(ATB_PARAM_PRIORITY > USER_ALLOWLISTED_PRIORITY)
assert.ok(ATB_PARAM_PRIORITY > SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY)

// NEWTAB_TRACKER_STATS is higher than allowlist
assert.ok(NEWTAB_TRACKER_STATS_REDIRECT_PRIORITY > UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY)
assert.ok(NEWTAB_TRACKER_STATS_REDIRECT_PRIORITY > USER_ALLOWLISTED_PRIORITY)
assert.ok(NEWTAB_TRACKER_STATS_REDIRECT_PRIORITY > SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY)

// Ceiling priorities should always be higher than baseline.
assert.ok(TRACKER_BLOCKING_BASELINE_PRIORITY < TRACKER_BLOCKING_CEILING_PRIORITY)
Expand All @@ -86,7 +82,6 @@ describe('Rule Priorities', () => {
assert.ok(SMARTER_ENCRYPTION_PRIORITY < GPC_HEADER_PRIORITY)
assert.ok(SMARTER_ENCRYPTION_PRIORITY < TRACKING_PARAM_PRIORITY)
assert.ok(SMARTER_ENCRYPTION_PRIORITY < UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY)
assert.ok(SMARTER_ENCRYPTION_PRIORITY < SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY)
assert.ok(SMARTER_ENCRYPTION_PRIORITY < USER_ALLOWLISTED_PRIORITY)
assert.ok(SMARTER_ENCRYPTION_PRIORITY < NEWTAB_TRACKER_STATS_REDIRECT_PRIORITY)

Expand All @@ -99,7 +94,6 @@ describe('Rule Priorities', () => {
assert.ok(TRACKER_ALLOWLIST_CEILING_PRIORITY < GPC_HEADER_PRIORITY)
assert.ok(TRACKER_ALLOWLIST_CEILING_PRIORITY < TRACKING_PARAM_PRIORITY)
assert.ok(TRACKER_ALLOWLIST_CEILING_PRIORITY < UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY)
assert.ok(TRACKER_ALLOWLIST_CEILING_PRIORITY < SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY)
assert.ok(TRACKER_ALLOWLIST_CEILING_PRIORITY < USER_ALLOWLISTED_PRIORITY)

// Ad attribution allowlisting and the contentBlocking allowlist should
Expand All @@ -112,7 +106,6 @@ describe('Rule Priorities', () => {
assert.ok(AD_ATTRIBUTION_POLICY_PRIORITY < GPC_HEADER_PRIORITY)
assert.ok(AD_ATTRIBUTION_POLICY_PRIORITY < TRACKING_PARAM_PRIORITY)
assert.ok(AD_ATTRIBUTION_POLICY_PRIORITY < UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY)
assert.ok(AD_ATTRIBUTION_POLICY_PRIORITY < SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY)
assert.ok(AD_ATTRIBUTION_POLICY_PRIORITY < USER_ALLOWLISTED_PRIORITY)

// Tracking parameter protection, AMP protection and GPC should take
Expand All @@ -125,13 +118,10 @@ describe('Rule Priorities', () => {
assert.ok(TRACKING_PARAM_PRIORITY > TRACKER_ALLOWLIST_CEILING_PRIORITY)
assert.ok(TRACKING_PARAM_PRIORITY > CONTENT_BLOCKING_ALLOWLIST_PRIORITY)
assert.ok(TRACKING_PARAM_PRIORITY < UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY)
assert.ok(TRACKING_PARAM_PRIORITY < SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY)
assert.ok(TRACKING_PARAM_PRIORITY < USER_ALLOWLISTED_PRIORITY)

// The unprotectedTemporary allowlist, exception for ServiceWorker
// initiated requests and the user allowlist should take priority over
// everything else.
assert.equal(UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY, SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY)
// The unprotectedTemporary allowlist and the user allowlist should take
// priority over everything else.
assert.equal(UNPROTECTED_TEMPORARY_ALLOWLIST_PRIORITY, USER_ALLOWLISTED_PRIORITY)
assert.ok(USER_ALLOWLISTED_PRIORITY > TRACKER_BLOCKING_CEILING_PRIORITY)
assert.ok(USER_ALLOWLISTED_PRIORITY > TRACKER_ALLOWLIST_CEILING_PRIORITY)
Expand Down
4 changes: 0 additions & 4 deletions shared/js/background/dnr-config-rulesets.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import tdsStorage from './storage/tds'
import trackers from './trackers'
import { isFeatureEnabled } from './utils'
import { ensureGPCHeaderRule } from './dnr-gpc'
import {
ensureServiceWorkerInitiatedRequestExceptions
} from './dnr-service-worker-initiated'
import { getDenylistedDomains } from './dnr-user-allowlist'
import { findExistingDynamicRule, SETTING_PREFIX, ruleIdRangeByConfigName } from './dnr-utils'
import {
Expand Down Expand Up @@ -292,7 +289,6 @@ export async function onConfigUpdate (configName, etag, configValue) {
} else if (configName === 'config') {
await updateExtensionConfigRules(etag, configValue)
await ensureGPCHeaderRule(configValue)
await ensureServiceWorkerInitiatedRequestExceptions(configValue)
}
// combined rules (cookie blocking)
await updateCombinedConfigBlocklistRules()
Expand Down
54 changes: 0 additions & 54 deletions shared/js/background/dnr-service-worker-initiated.js

This file was deleted.

67 changes: 0 additions & 67 deletions unit-test/background/declarative-net-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import tabManager from '../../shared/js/background/tab-manager'
import tdsStorage from '../../shared/js/background/storage/tds'
import trackers from '../../shared/js/background/trackers'
import { ensureGPCHeaderRule } from '../../shared/js/background/dnr-gpc'
import {
ensureServiceWorkerInitiatedRequestExceptions
} from '../../shared/js/background/dnr-service-worker-initiated'
import {
onConfigUpdate
} from '../../shared/js/background/dnr-config-rulesets'
Expand All @@ -31,7 +28,6 @@ import {
SETTING_PREFIX
} from '../../shared/js/background/dnr-utils'
import {
SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY,
USER_ALLOWLISTED_PRIORITY
} from '@duckduckgo/ddg2dnr/lib/rulePriorities'
import { GPC_HEADER_PRIORITY } from '@duckduckgo/ddg2dnr/lib/gpc'
Expand Down Expand Up @@ -691,69 +687,6 @@ describe('declarativeNetRequest', () => {
})
})

it('ServiceWorker initiated request allowing', async () => {
const ruleId = SERVICE_WORKER_INITIATED_ALLOWING_RULE_ID
const rulePriority = SERVICE_WORKER_INITIATED_ALLOWING_PRIORITY
const rule = {
id: ruleId,
priority: rulePriority,
action: { type: 'allow' },
condition: { tabIds: [-1], initiatorDomains: ['example.com', 'google.com', 'suntrust.com'] }
}
const tempUnprotected = config.unprotectedTemporary

// The rule won't exist initially.
expect(updateSessionRulesObserver.calls.count()).toEqual(0)
expect(sessionRulesByRuleId.has(ruleId)).toBeFalse()

// if there are no serviceworker exceptions, or unprotected sites, no rule should be created
config.features.serviceworkerInitiatedRequests = {
state: 'enabled',
exceptions: []
}
config.unprotectedTemporary = []
await ensureServiceWorkerInitiatedRequestExceptions(config)
expect(updateSessionRulesObserver.calls.count()).toEqual(1)
expect(sessionRulesByRuleId.has(ruleId)).toBeFalse()

// If completely disabled, a rule should be created with no domain
// exceptions.
config.features.serviceworkerInitiatedRequests = {
state: 'disabled',
exceptions: [{ domain: 'example.com', reason: '' }]
}
config.unprotectedTemporary = tempUnprotected
await ensureServiceWorkerInitiatedRequestExceptions(config)
expect(updateSessionRulesObserver.calls.count()).toEqual(2)
expect(sessionRulesByRuleId.has(ruleId)).toBeTrue()
expect(sessionRulesByRuleId.get(ruleId)?.condition?.initiatorDomains).toBeUndefined()

// But if enabled, domain exceptions should be used.
config.features.serviceworkerInitiatedRequests.state = 'enabled'
await ensureServiceWorkerInitiatedRequestExceptions(config)
expect(updateSessionRulesObserver.calls.count()).toEqual(3)
expect(sessionRulesByRuleId.has(ruleId)).toBeTrue()
expect(sessionRulesByRuleId.get(ruleId)).toEqual(rule)
expect(sessionRulesByRuleId.get(ruleId)?.condition?.initiatorDomains[0]).toEqual('example.com')

// If ensureServiceWorkerInitiatedRequestException is called again, the
// rule should just be recreated.
await ensureServiceWorkerInitiatedRequestExceptions(config)
expect(updateSessionRulesObserver.calls.count()).toEqual(4)
expect(sessionRulesByRuleId.has(ruleId)).toBeTrue()
expect(sessionRulesByRuleId.get(ruleId)).toEqual(rule)

// If enabled and there are no domain exceptions, the rule should be
// removed.
config.features.serviceworkerInitiatedRequests.exceptions = []
config.unprotectedTemporary = []
await ensureServiceWorkerInitiatedRequestExceptions(config)
expect(updateSessionRulesObserver.calls.count()).toEqual(5)
expect(sessionRulesByRuleId.has(ruleId)).toBeFalse()

config.unprotectedTemporary = tempUnprotected
})

it('GPC header redirection', async () => {
const ruleId = GPC_HEADER_RULE_ID
const rulePriority = GPC_HEADER_PRIORITY
Expand Down

0 comments on commit 7dca719

Please sign in to comment.