Skip to content

Commit 2d9e5e4

Browse files
authored
Harmful API protections (#527)
* Add a utility function for overriding methods * Add a utility function to wrap constructor descriptors * Ignore empty objects in wrapper utils * Remove unused utilities for now * Auto-deny motion sensor permissions * WIP add harmful-apis protection to the mv3 extension * Throw an error in Sensor API * Reduce entropy on UA Client Hints * remove NetworkInformation API * block navigator.getInstalledRelatedApps * Remove File System Access API * Mock Screen.isExtended * More consistent naming * Support WorkerNavigator in harmful API protection * Make web bluetooth block cross-platform * Auto-deny more permissions * Make WebUSB platform-agnostic * Move WebSerial block out of windows-specific code * Move WebHID API from the windows code, and do not throw errors from it * Move WebMIDI block out of windows code * Remove Idle Detection API * Remove Web NFC * Filter some events from harmful APIs * Filter storage quota estimate to reduce FP entropy * Fix a linting error * Post-rebase fix * Use wrap functions in harmful API protections * Filter events from some harmful APIs * Ignore non-existing APIs * Move permission filters into corresponding methods * Add some comments for ContentFeature methods * Make harmful API protections configurable * Add utils for wrapping functions * Remove dead code * disallow overriding dom0 event handlers * Workaround for xrays in firefox * Move harmful api permissions tests * Fix UA hints filter * Add playwright tests for harmful APIs * Fix WebBluetooth event filter * HID.requestDevice should return a Promise * Fix a race condition in the playwright test * Use Reflect.apply instead of .call * Linting error * Enable harmful api tests for windows builds * tweak gitignore * Do not fail test when some hardware is unavailable * Windows: fix an exception in insecure contexts * Remove debug log * Protect agains potential undefined feature config * refactor the test structure * re-enable windows permission tests * lint fix * Increase the bundle size threshold * Add some docs for harmful API prtotection * Change the harmful APIs config for consistency * Fix docs generation (ContentFeature is not included in typedoc yet)
1 parent d7415b9 commit 2d9e5e4

File tree

16 files changed

+1013
-77
lines changed

16 files changed

+1013
-77
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
node_modules/
33
.swiftpm
44
.env
5+
.build/
56
build/
67
docs/
78
test-results/
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { test, expect } from '@playwright/test'
2+
import { readFileSync } from 'fs'
3+
import { mockWindowsMessaging, wrapWindowsScripts } from '@duckduckgo/messaging/lib/test-utils.mjs'
4+
import { perPlatform } from './type-helpers.mjs'
5+
6+
test('Harmful APIs protections', async ({ page }, testInfo) => {
7+
const protection = HarmfulApisSpec.create(page, testInfo)
8+
await protection.enabled()
9+
const results = await protection.runTests();
10+
// note that if protections are disabled, the browser will show a device selection pop-up, which will never be dismissed
11+
12+
[
13+
'deviceOrientation',
14+
'GenericSensor',
15+
'UaClientHints',
16+
'NetworkInformation',
17+
'getInstalledRelatedApps',
18+
'FileSystemAccess',
19+
'WindowPlacement',
20+
'WebBluetooth',
21+
'WebUsb',
22+
'WebSerial',
23+
'WebHid',
24+
'WebMidi',
25+
'IdleDetection',
26+
'WebNfc',
27+
'StorageManager'
28+
].forEach((name) => {
29+
for (const result of results[name]) {
30+
expect(result.result).toEqual(result.expected)
31+
}
32+
})
33+
})
34+
35+
export class HarmfulApisSpec {
36+
htmlPage = '/harmful-apis/index.html'
37+
config = './integration-test/test-pages/harmful-apis/config/apis.json'
38+
39+
/**
40+
* @param {import("@playwright/test").Page} page
41+
* @param {import("./type-helpers.mjs").Build} build
42+
* @param {import("./type-helpers.mjs").PlatformInfo} platform
43+
*/
44+
constructor (page, build, platform) {
45+
this.page = page
46+
this.build = build
47+
this.platform = platform
48+
}
49+
50+
async enabled () {
51+
await this.installPolyfills()
52+
const config = JSON.parse(readFileSync(this.config, 'utf8'))
53+
await this.setup({ config })
54+
await this.page.goto(this.htmlPage)
55+
}
56+
57+
async runTests () {
58+
for (const button of await this.page.getByTestId('user-gesture-button').all()) {
59+
await button.click()
60+
}
61+
const resultsPromise = this.page.evaluate(() => {
62+
return new Promise(resolve => {
63+
window.addEventListener('results-ready', () => {
64+
// @ts-expect-error - this is added by the test framework
65+
resolve(window.results)
66+
})
67+
})
68+
})
69+
await this.page.getByTestId('render-results').click()
70+
return await resultsPromise
71+
}
72+
73+
/**
74+
* In CI, the global objects such as USB might not be installed on the
75+
* version of chromium running there.
76+
*/
77+
async installPolyfills () {
78+
await this.page.addInitScript(() => {
79+
// @ts-expect-error - testing
80+
if (typeof Bluetooth === 'undefined') {
81+
globalThis.Bluetooth = {}
82+
globalThis.Bluetooth.prototype = { requestDevice: async () => { /* noop */ } }
83+
}
84+
// @ts-expect-error - testing
85+
if (typeof USB === 'undefined') {
86+
globalThis.USB = {}
87+
globalThis.USB.prototype = { requestDevice: async () => { /* noop */ } }
88+
}
89+
90+
// @ts-expect-error - testing
91+
if (typeof Serial === 'undefined') {
92+
globalThis.Serial = {}
93+
globalThis.Serial.prototype = { requestPort: async () => { /* noop */ } }
94+
}
95+
// @ts-expect-error - testing
96+
if (typeof HID === 'undefined') {
97+
globalThis.HID = {}
98+
globalThis.HID.prototype = { requestDevice: async () => { /* noop */ } }
99+
}
100+
})
101+
}
102+
103+
/**
104+
* @param {object} params
105+
* @param {Record<string, any>} params.config
106+
* @return {Promise<void>}
107+
*/
108+
async setup (params) {
109+
const { config } = params
110+
111+
// read the built file from disk and do replacements
112+
const injectedJS = wrapWindowsScripts(this.build.artifact, {
113+
$CONTENT_SCOPE$: config,
114+
$USER_UNPROTECTED_DOMAINS$: [],
115+
$USER_PREFERENCES$: {
116+
platform: { name: 'windows' },
117+
debug: true
118+
}
119+
})
120+
121+
await this.page.addInitScript(mockWindowsMessaging, {
122+
messagingContext: {
123+
env: 'development',
124+
context: 'contentScopeScripts',
125+
featureName: 'n/a'
126+
},
127+
responses: {}
128+
})
129+
130+
// attach the JS
131+
await this.page.addInitScript(injectedJS)
132+
}
133+
134+
/**
135+
* Helper for creating an instance per platform
136+
* @param {import("@playwright/test").Page} page
137+
* @param {import("@playwright/test").TestInfo} testInfo
138+
*/
139+
static create (page, testInfo) {
140+
// Read the configuration object to determine which platform we're testing against
141+
const { platformInfo, build } = perPlatform(testInfo.project.use)
142+
return new HarmfulApisSpec(page, build, platformInfo)
143+
}
144+
}

integration-test/playwright/windows-permissions.spec.js

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,42 +31,11 @@ export class WindowsPermissionsSpec {
3131
}
3232

3333
async enabled () {
34-
await this.installPolyfills()
3534
const config = JSON.parse(readFileSync(this.config, 'utf8'))
3635
await this.setup({ config })
3736
await this.page.goto(this.htmlPage)
3837
}
3938

40-
/**
41-
* In CI, the global objects such as USB might not be installed on the
42-
* version of chromium running there.
43-
*/
44-
async installPolyfills () {
45-
await this.page.addInitScript(() => {
46-
// @ts-expect-error - testing
47-
if (typeof Bluetooth === 'undefined') {
48-
globalThis.Bluetooth = {}
49-
globalThis.Bluetooth.prototype = { requestDevice: async () => { /* noop */ } }
50-
}
51-
// @ts-expect-error - testing
52-
if (typeof USB === 'undefined') {
53-
globalThis.USB = {}
54-
globalThis.USB.prototype = { requestDevice: async () => { /* noop */ } }
55-
}
56-
57-
// @ts-expect-error - testing
58-
if (typeof Serial === 'undefined') {
59-
globalThis.Serial = {}
60-
globalThis.Serial.prototype = { requestPort: async () => { /* noop */ } }
61-
}
62-
// @ts-expect-error - testing
63-
if (typeof HID === 'undefined') {
64-
globalThis.HID = {}
65-
globalThis.HID.prototype = { requestDevice: async () => { /* noop */ } }
66-
}
67-
})
68-
}
69-
7039
/**
7140
* @param {object} params
7241
* @param {Record<string, any>} params.config
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"unprotectedTemporary": [],
3+
"features": {
4+
"harmfulApis": {
5+
"state": "enabled",
6+
"settings": {
7+
"deviceOrientation": {
8+
"state": "enabled",
9+
"filterEvents": ["deviceorientation", "devicemotion"]
10+
},
11+
"GenericSensor": {
12+
"state": "enabled",
13+
"filterPermissions": [
14+
"accelerometer",
15+
"ambient-light-sensor",
16+
"gyroscope",
17+
"magnetometer"
18+
],
19+
"blockSensorStart": true
20+
},
21+
"UaClientHints": {
22+
"state": "enabled",
23+
"highEntropyValues": {
24+
"trimBrands": true,
25+
"trimPlatformVersion": 2,
26+
"trimUaFullVersion": 1,
27+
"trimFullVersionList": 1,
28+
"model": "overridden-model",
29+
"architecture": "overridden-architecture",
30+
"bitness": "overridden-bitness",
31+
"platform": "overridden-platform",
32+
"mobile": "overridden-mobile"
33+
}
34+
},
35+
"NetworkInformation": {
36+
"state": "enabled"
37+
},
38+
"getInstalledRelatedApps": {
39+
"state": "enabled",
40+
"returnValue": ["overridden-return-value"]
41+
},
42+
"FileSystemAccess": {
43+
"state": "enabled",
44+
"disableOpenFilePicker": true,
45+
"disableSaveFilePicker": true,
46+
"disableDirectoryPicker": true,
47+
"disableGetAsFileSystemHandle": true
48+
},
49+
"WindowPlacement": {
50+
"state": "enabled",
51+
"filterPermissions": ["window-placement", "window-management"],
52+
"screenIsExtended": true
53+
},
54+
"WebBluetooth": {
55+
"state": "enabled",
56+
"filterPermissions": ["bluetooth"],
57+
"filterEvents": ["availabilitychanged"],
58+
"blockGetAvailability": true,
59+
"blockRequestDevice": true
60+
},
61+
"WebUsb": {
62+
"state": "enabled"
63+
},
64+
"WebSerial": {
65+
"state": "enabled"
66+
},
67+
"WebHid": {
68+
"state": "enabled"
69+
},
70+
"WebMidi": {
71+
"state": "enabled",
72+
"filterPermissions": ["midi"]
73+
},
74+
"IdleDetection": {
75+
"state": "enabled",
76+
"filterPermissions": ["idle-detection"]
77+
},
78+
"WebNfc": {
79+
"state": "enabled",
80+
"disableNdefReader": true,
81+
"disableNdefMessage": true,
82+
"disableNdefRecord": true
83+
},
84+
"StorageManager": {
85+
"state": "enabled",
86+
"allowedQuotaValues": [1337]
87+
},
88+
"domains": []
89+
},
90+
"exceptions": []
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)