From bee410682f117595f96c509241e2d977acd1f5c8 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:13:15 +0200 Subject: [PATCH 1/9] Initial PoC windows brand hints override --- .../config/brand-override.json | 23 +++ .../test-pages/windows-brand-hints/index.html | 15 ++ .../pages/brand-override.html | 139 ++++++++++++++++++ .../windows-brand-hints/pages/quick-test.html | 111 ++++++++++++++ .../windows-brand-hints.spec.js | 19 +++ injected/playwright.config.js | 1 + injected/src/features.js | 2 + injected/src/features/windows-brand-hints.js | 62 ++++++++ 8 files changed, 372 insertions(+) create mode 100644 injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json create mode 100644 injected/integration-test/test-pages/windows-brand-hints/index.html create mode 100644 injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html create mode 100644 injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html create mode 100644 injected/integration-test/windows-brand-hints.spec.js create mode 100644 injected/src/features/windows-brand-hints.js diff --git a/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json b/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json new file mode 100644 index 0000000000..c344a16bc7 --- /dev/null +++ b/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json @@ -0,0 +1,23 @@ +{ + "readme": "This config tests that windowsBrandHints can override navigator.userAgentData.brands", + "version": 1, + "unprotectedTemporary": [], + "features": { + "windowsBrandHints": { + "state": "enabled", + "exceptions": [], + "settings": { + "brands": [ + { + "brand": "Chromium", + "version": "140" + }, + { + "brand": "DuckDuckGo", + "version": "140" + } + ] + } + } + } +} diff --git a/injected/integration-test/test-pages/windows-brand-hints/index.html b/injected/integration-test/test-pages/windows-brand-hints/index.html new file mode 100644 index 0000000000..332f55e546 --- /dev/null +++ b/injected/integration-test/test-pages/windows-brand-hints/index.html @@ -0,0 +1,15 @@ + + +
+ + +This page verifies that navigator.userAgentData.brands can be overridden by the windowsBrandHints feature
+ + + + diff --git a/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html b/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html new file mode 100644 index 0000000000..6e82664a86 --- /dev/null +++ b/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html @@ -0,0 +1,111 @@ + + + + + +This page verifies that navigator.userAgentData.brands can be overridden by the windowsBrandHints feature
- - - - diff --git a/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json b/injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json similarity index 95% rename from injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json rename to injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json index c344a16bc7..ceb92a2785 100644 --- a/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json +++ b/injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json @@ -3,7 +3,7 @@ "version": 1, "unprotectedTemporary": [], "features": { - "windowsBrandHints": { + "windowsChBrands": { "state": "enabled", "exceptions": [], "settings": { diff --git a/injected/integration-test/test-pages/windows-brand-hints/index.html b/injected/integration-test/test-pages/windows-ch-brands/index.html similarity index 85% rename from injected/integration-test/test-pages/windows-brand-hints/index.html rename to injected/integration-test/test-pages/windows-ch-brands/index.html index 332f55e546..baa3d2831d 100644 --- a/injected/integration-test/test-pages/windows-brand-hints/index.html +++ b/injected/integration-test/test-pages/windows-ch-brands/index.html @@ -3,11 +3,13 @@ -Windows CH Brands
diff --git a/injected/integration-test/test-pages/windows-ch-brands/pages/brand-override.html b/injected/integration-test/test-pages/windows-ch-brands/pages/brand-override.html new file mode 100644 index 0000000000..5be97c7cec --- /dev/null +++ b/injected/integration-test/test-pages/windows-ch-brands/pages/brand-override.html @@ -0,0 +1,103 @@ + + + + + +This page verifies that navigator.userAgentData.brands uses the configured brands from the windowsChBrands feature settings
+ + + + diff --git a/injected/integration-test/windows-brand-hints.spec.js b/injected/integration-test/windows-ch-brands.spec.js similarity index 70% rename from injected/integration-test/windows-brand-hints.spec.js rename to injected/integration-test/windows-ch-brands.spec.js index 8a0ef5b685..825afc2bb6 100644 --- a/injected/integration-test/windows-brand-hints.spec.js +++ b/injected/integration-test/windows-ch-brands.spec.js @@ -1,11 +1,11 @@ import { test, expect } from '@playwright/test'; import { ResultsCollector } from './page-objects/results-collector.js'; -test('Windows Brand Hints override', async ({ page }, testInfo) => { +test('Windows CH Brands override', async ({ page }, testInfo) => { const collector = ResultsCollector.create(page, testInfo.project.use); await collector.load( - '/windows-brand-hints/pages/brand-override.html', - './integration-test/test-pages/windows-brand-hints/config/brand-override.json', + '/windows-ch-brands/pages/brand-override.html', + './integration-test/test-pages/windows-ch-brands/config/brand-override.json', ); const results = await collector.results(); diff --git a/injected/playwright.config.js b/injected/playwright.config.js index 272349d03c..f48ccede34 100644 --- a/injected/playwright.config.js +++ b/injected/playwright.config.js @@ -10,7 +10,7 @@ export default defineConfig({ 'integration-test/duckplayer-remote-config.spec.js', 'integration-test/harmful-apis.spec.js', 'integration-test/windows-permissions.spec.js', - 'integration-test/windows-brand-hints.spec.js', + 'integration-test/windows-ch-brands.spec.js', 'integration-test/broker-protection-tests/**/*.spec.js', 'integration-test/breakage-reporting.spec.js', 'integration-test/duck-ai-data-clearing.spec.js', diff --git a/injected/src/features.js b/injected/src/features.js index e8e1c0b133..e4bb48b3de 100644 --- a/injected/src/features.js +++ b/injected/src/features.js @@ -26,7 +26,7 @@ const otherFeatures = /** @type {const} */ ([ 'harmfulApis', 'webCompat', 'windowsPermissionUsage', - 'windowsBrandHints', + 'windowsChBrands', 'brokerProtection', 'performanceMetrics', 'breakageReporting', @@ -69,7 +69,7 @@ export const platformSupport = { ...baseFeatures, 'webTelemetry', 'windowsPermissionUsage', - 'windowsBrandHints', + 'windowsChBrands', 'duckPlayer', 'brokerProtection', 'breakageReporting', diff --git a/injected/src/features/windows-brand-hints.js b/injected/src/features/windows-brand-hints.js deleted file mode 100644 index faa4ee849b..0000000000 --- a/injected/src/features/windows-brand-hints.js +++ /dev/null @@ -1,56 +0,0 @@ -import ContentFeature from '../content-feature'; - -export default class WindowsBrandHints extends ContentFeature { - init() { - const brandsConfig = this.getFeatureSetting('brands'); - - if (!brandsConfig || brandsConfig.length === 0) { - return; - } - - this.shimUserAgentDataBrands(brandsConfig); - } - - /** - * Override navigator.userAgentData.brands to match the Sec-CH-UA header we're sending. - * @param {Array<{brand: string, version: string}>} brandsConfig - Array of brand objects from config - */ - shimUserAgentDataBrands(brandsConfig) { - try { - if (!navigator.userAgentData || !navigator.userAgentData.brands) { - return; - } - - const originalBrands = navigator.userAgentData.brands; - - // Keep GREASE value from original brands - const greaseValue = originalBrands.find(b => b.brand.includes('Not') && b.brand.includes('Brand')); - - const newBrands = [...brandsConfig]; - if (greaseValue) { - newBrands.push(greaseValue); - } - - // The brands property is on NavigatorUAData.prototype, not the instance - const proto = Object.getPrototypeOf(navigator.userAgentData); - - this.wrapProperty(proto, 'brands', { - get: () => newBrands - }); - - if (proto.getHighEntropyValues) { - this.wrapMethod(proto, 'getHighEntropyValues', (originalFn, ...args) => { - return originalFn.apply(navigator.userAgentData, args).then(result => { - if (args[0] && args[0].includes('brands')) { - result.brands = newBrands; - } - return result; - }); - }); - } - - } catch (error) { - // TODO: log error somewhere? - } - } -} \ No newline at end of file diff --git a/injected/src/features/windows-ch-brands.js b/injected/src/features/windows-ch-brands.js new file mode 100644 index 0000000000..c5f597c774 --- /dev/null +++ b/injected/src/features/windows-ch-brands.js @@ -0,0 +1,102 @@ +import ContentFeature from '../content-feature'; + +export default class WindowsChBrands extends ContentFeature { + constructor(featureName, importConfig, args) { + super(featureName, importConfig, args); + + this.cachedBrands = null; + this.originalBrands = null; + } + + init() { + this.updateConfig(); + this.shimUserAgentDataBrands(); + } + + updateConfig() { + // Clear cache when privacy config updated + this.cachedBrands = null; + this.log.info('Privacy remote config updated'); + } + + /** + * Override navigator.userAgentData.brands to match the Sec-CH-UA header + */ + shimUserAgentDataBrands() { + try { + // @ts-expect-error - userAgentData not yet standard + if (!navigator.userAgentData || !navigator.userAgentData.brands) { + return; + } + + if (!this.originalBrands) { + // @ts-expect-error - userAgentData not yet standard + this.originalBrands = [...navigator.userAgentData.brands]; + } + + if (this.cachedBrands) { + this.applyBrandsOverride(this.cachedBrands); + return; + } + + const mutatedBrands = this.applyBrandMutations(); + + if (mutatedBrands) { + this.cachedBrands = mutatedBrands; + this.applyBrandsOverride(mutatedBrands); + } + } catch (error) { + this.log.error('Error in shimUserAgentDataBrands:', error); + } + } + + /** + * Apply brand mutations using the configured brands from feature settings + * @returns {Array<{brand: string, version: string}>|null} - Configured brands or null if no changes + */ + applyBrandMutations() { + const configuredBrands = this.getFeatureSetting('brands'); + + if (!configuredBrands || configuredBrands.length === 0) { + this.log.info('No CH brands configured, skipping mutations'); + return null; + } + + this.log.info('Applying configured brands:', configuredBrands); + return configuredBrands; + } + + /** + * Apply the brand override to navigator.userAgentData.brands + * @param {Array<{brand: string, version: string}>} newBrands - Brands to apply + */ + applyBrandsOverride(newBrands) { + // @ts-expect-error - userAgentData not yet standard + const proto = Object.getPrototypeOf(navigator.userAgentData); + + this.wrapProperty(proto, 'brands', { + get: () => newBrands, + }); + + // Also override getHighEntropyValues on the prototype + if (proto.getHighEntropyValues) { + this.wrapMethod(proto, 'getHighEntropyValues', (originalFn, ...args) => { + // @ts-expect-error - userAgentData not yet standard + return originalFn.apply(navigator.userAgentData, args).then((result) => { + if (args[0] && args[0].includes('brands')) { + result.brands = newBrands; + } + return result; + }); + }); + } + } + + /** + * Handle configuration updates (called when remote config changes) + */ + update() { + this.updateConfig(); + this.shimUserAgentDataBrands(); + } +} \ No newline at end of file From dbdebd95afd9546a60be62810047105f8a7844a9 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:02:22 +0200 Subject: [PATCH 4/9] Fix errant space --- injected/integration-test/pages.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/injected/integration-test/pages.spec.js b/injected/integration-test/pages.spec.js index 9d41fe26f2..d6b02bff2c 100644 --- a/injected/integration-test/pages.spec.js +++ b/injected/integration-test/pages.spec.js @@ -177,5 +177,4 @@ test.describe('Test integration pages', () => { { version: 99 }, ); }); - }); From d1427357fd738ae7a5e663ec2e15b7b27848f215 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:58:48 +0100 Subject: [PATCH 5/9] Return early if brands config is missing or malformed --- .../config/brands-missing.json | 13 ++++ .../test-pages/windows-ch-brands/index.html | 1 + .../pages/brands-missing.html | 71 +++++++++++++++++++ .../windows-ch-brands.spec.js | 17 +++++ injected/src/features/windows-ch-brands.js | 7 ++ 5 files changed, 109 insertions(+) create mode 100644 injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json create mode 100644 injected/integration-test/test-pages/windows-ch-brands/pages/brands-missing.html diff --git a/injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json b/injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json new file mode 100644 index 0000000000..87271bb3af --- /dev/null +++ b/injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json @@ -0,0 +1,13 @@ +{ + "readme": "This config tests that windowsChBrands feature returns early when brands setting is missing (feature is enabled but settings.brands is undefined)", + "version": 1, + "unprotectedTemporary": [], + "features": { + "windowsChBrands": { + "state": "enabled", + "exceptions": [], + "settings": { + } + } + } +} diff --git a/injected/integration-test/test-pages/windows-ch-brands/index.html b/injected/integration-test/test-pages/windows-ch-brands/index.html index baa3d2831d..5445632208 100644 --- a/injected/integration-test/test-pages/windows-ch-brands/index.html +++ b/injected/integration-test/test-pages/windows-ch-brands/index.html @@ -12,6 +12,7 @@Windows CH Brands