From 61e57be42dd05a4d459cca2cccac4b68cb659078 Mon Sep 17 00:00:00 2001 From: gsinafirooz Date: Wed, 4 Jan 2023 16:48:37 -0800 Subject: [PATCH] Reject Web Bluetooth requests with an opaque origin The Web Bluetooth API tracks permissions using the origin of the top-level document in the frame tree. If this document has an opaque origin then there is no way to format the origin for display to the user in permission prompts or to write their decision in the preferences file. Access to the Web Bluetooth API from such contexts should therefore be blocked. Bug: 1375133 Change-Id: Idf737c1806eac4342e0fe716e2561e51aa127f53 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4113162 Reviewed-by: Reilly Grant Commit-Queue: Sina Firoozabadi Cr-Commit-Position: refs/heads/main@{#1089042} --- .../reject_opaque_origin.https.html | 13 +++++++ .../reject_opaque_origin.https.html.headers | 1 + .../sandboxed_iframe.https.window.js | 27 +++++++++++++++ .../reject_opaque_origin.https.html | 13 +++++++ .../reject_opaque_origin.https.html.headers | 1 + .../sandboxed_iframe.https.window.js | 27 +++++++++++++++ .../cross-origin-iframe.sub.https.window.js | 2 +- .../reject_opaque_origin.https.html | 13 +++++++ .../reject_opaque_origin.https.html.headers | 1 + ...uest-from-sandboxed-iframe.https.window.js | 3 +- .../sandboxed_iframe.https.window.js | 27 +++++++++++++++ .../reject_opaque_origin.https.html | 13 +++++++ .../reject_opaque_origin.https.html.headers | 1 + .../sandboxed_iframe.https.window.js | 27 +++++++++++++++ .../resources/health-thermometer-iframe.html | 34 +++++++++++++++---- 15 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 bluetooth/getAvailability/reject_opaque_origin.https.html create mode 100644 bluetooth/getAvailability/reject_opaque_origin.https.html.headers create mode 100644 bluetooth/getAvailability/sandboxed_iframe.https.window.js create mode 100644 bluetooth/getDevices/reject_opaque_origin.https.html create mode 100644 bluetooth/getDevices/reject_opaque_origin.https.html.headers create mode 100644 bluetooth/getDevices/sandboxed_iframe.https.window.js create mode 100644 bluetooth/requestDevice/reject_opaque_origin.https.html create mode 100644 bluetooth/requestDevice/reject_opaque_origin.https.html.headers create mode 100644 bluetooth/requestDevice/sandboxed_iframe.https.window.js create mode 100644 bluetooth/requestLEScan/reject_opaque_origin.https.html create mode 100644 bluetooth/requestLEScan/reject_opaque_origin.https.html.headers create mode 100644 bluetooth/requestLEScan/sandboxed_iframe.https.window.js diff --git a/bluetooth/getAvailability/reject_opaque_origin.https.html b/bluetooth/getAvailability/reject_opaque_origin.https.html new file mode 100644 index 00000000000000..8745fc9551bba2 --- /dev/null +++ b/bluetooth/getAvailability/reject_opaque_origin.https.html @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/bluetooth/getAvailability/reject_opaque_origin.https.html.headers b/bluetooth/getAvailability/reject_opaque_origin.https.html.headers new file mode 100644 index 00000000000000..c7e4e7cc5bd3fa --- /dev/null +++ b/bluetooth/getAvailability/reject_opaque_origin.https.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts \ No newline at end of file diff --git a/bluetooth/getAvailability/sandboxed_iframe.https.window.js b/bluetooth/getAvailability/sandboxed_iframe.https.window.js new file mode 100644 index 00000000000000..c5e3d1e89038cf --- /dev/null +++ b/bluetooth/getAvailability/sandboxed_iframe.https.window.js @@ -0,0 +1,27 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/bluetooth/resources/bluetooth-test.js +// META: script=/bluetooth/resources/bluetooth-fake-devices.js + +'use strict'; + +let iframe = document.createElement('iframe'); + +bluetooth_test(async () => { + await getConnectedHealthThermometerDevice(); + await new Promise(resolve => { + iframe.src = '/bluetooth/resources/health-thermometer-iframe.html'; + iframe.sandbox.add('allow-scripts'); + iframe.allow = 'bluetooth'; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve); + }); + await new Promise(resolve => { + iframe.contentWindow.postMessage({type: 'GetAvailability'}, '*'); + + window.addEventListener('message', (messageEvent) => { + assert_false(/^FAIL: .*/.test(messageEvent.data)); + resolve(); + }); + }); +}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file diff --git a/bluetooth/getDevices/reject_opaque_origin.https.html b/bluetooth/getDevices/reject_opaque_origin.https.html new file mode 100644 index 00000000000000..64b2808fbce366 --- /dev/null +++ b/bluetooth/getDevices/reject_opaque_origin.https.html @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/bluetooth/getDevices/reject_opaque_origin.https.html.headers b/bluetooth/getDevices/reject_opaque_origin.https.html.headers new file mode 100644 index 00000000000000..c7e4e7cc5bd3fa --- /dev/null +++ b/bluetooth/getDevices/reject_opaque_origin.https.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts \ No newline at end of file diff --git a/bluetooth/getDevices/sandboxed_iframe.https.window.js b/bluetooth/getDevices/sandboxed_iframe.https.window.js new file mode 100644 index 00000000000000..22cfd17d466489 --- /dev/null +++ b/bluetooth/getDevices/sandboxed_iframe.https.window.js @@ -0,0 +1,27 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/bluetooth/resources/bluetooth-test.js +// META: script=/bluetooth/resources/bluetooth-fake-devices.js + +'use strict'; + +let iframe = document.createElement('iframe'); + +bluetooth_test(async () => { + await getConnectedHealthThermometerDevice(); + await new Promise(resolve => { + iframe.src = '/bluetooth/resources/health-thermometer-iframe.html'; + iframe.sandbox.add('allow-scripts'); + iframe.allow = 'bluetooth'; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve); + }); + await new Promise(resolve => { + iframe.contentWindow.postMessage({type: 'GetDevices'}, '*'); + + window.addEventListener('message', (messageEvent) => { + assert_false(/^FAIL: .*/.test(messageEvent.data)); + resolve(); + }); + }); +}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file diff --git a/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js b/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js index a5e6fac25e3bf2..d802a862791e98 100644 --- a/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js +++ b/bluetooth/requestDevice/cross-origin-iframe.sub.https.window.js @@ -24,5 +24,5 @@ bluetooth_test(async (t) => { const messageEvent = await windowWatcher.wait_for('message'); assert_equals( messageEvent.data, - 'SecurityError: Failed to execute \'requestDevice\' on \'Bluetooth\': Access to the feature "bluetooth" is disallowed by permissions policy.'); + 'FAIL: SecurityError: Failed to execute \'requestDevice\' on \'Bluetooth\': Access to the feature "bluetooth" is disallowed by permissions policy.'); }, test_desc); diff --git a/bluetooth/requestDevice/reject_opaque_origin.https.html b/bluetooth/requestDevice/reject_opaque_origin.https.html new file mode 100644 index 00000000000000..df348dd39e0b62 --- /dev/null +++ b/bluetooth/requestDevice/reject_opaque_origin.https.html @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/bluetooth/requestDevice/reject_opaque_origin.https.html.headers b/bluetooth/requestDevice/reject_opaque_origin.https.html.headers new file mode 100644 index 00000000000000..c7e4e7cc5bd3fa --- /dev/null +++ b/bluetooth/requestDevice/reject_opaque_origin.https.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts \ No newline at end of file diff --git a/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js b/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js index 17a9da333a5d5c..2101cf0d6b7478 100644 --- a/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js +++ b/bluetooth/requestDevice/request-from-sandboxed-iframe.https.window.js @@ -5,7 +5,8 @@ 'use strict'; const test_desc = 'Request device from a unique origin. ' + 'Should reject with SecurityError.'; -const expected = 'SecurityError: Failed to execute \'requestDevice\' on ' + +const expected = + 'FAIL: SecurityError: Failed to execute \'requestDevice\' on ' + '\'Bluetooth\': Access to the feature "bluetooth" is disallowed by ' + 'permissions policy.'; diff --git a/bluetooth/requestDevice/sandboxed_iframe.https.window.js b/bluetooth/requestDevice/sandboxed_iframe.https.window.js new file mode 100644 index 00000000000000..e9192a9305b147 --- /dev/null +++ b/bluetooth/requestDevice/sandboxed_iframe.https.window.js @@ -0,0 +1,27 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/bluetooth/resources/bluetooth-test.js +// META: script=/bluetooth/resources/bluetooth-fake-devices.js + +'use strict'; + +let iframe = document.createElement('iframe'); + +bluetooth_test(async () => { + await getConnectedHealthThermometerDevice(); + await new Promise(resolve => { + iframe.src = '/bluetooth/resources/health-thermometer-iframe.html'; + iframe.sandbox.add('allow-scripts'); + iframe.allow = 'bluetooth'; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve); + }); + await new Promise(resolve => { + iframe.contentWindow.postMessage({type: 'RequestDevice'}, '*'); + + window.addEventListener('message', (messageEvent) => { + assert_false(/^FAIL: .*/.test(messageEvent.data)); + resolve(); + }); + }); +}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file diff --git a/bluetooth/requestLEScan/reject_opaque_origin.https.html b/bluetooth/requestLEScan/reject_opaque_origin.https.html new file mode 100644 index 00000000000000..272c5aa760cf7f --- /dev/null +++ b/bluetooth/requestLEScan/reject_opaque_origin.https.html @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/bluetooth/requestLEScan/reject_opaque_origin.https.html.headers b/bluetooth/requestLEScan/reject_opaque_origin.https.html.headers new file mode 100644 index 00000000000000..c7e4e7cc5bd3fa --- /dev/null +++ b/bluetooth/requestLEScan/reject_opaque_origin.https.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts \ No newline at end of file diff --git a/bluetooth/requestLEScan/sandboxed_iframe.https.window.js b/bluetooth/requestLEScan/sandboxed_iframe.https.window.js new file mode 100644 index 00000000000000..32d1e74b778d99 --- /dev/null +++ b/bluetooth/requestLEScan/sandboxed_iframe.https.window.js @@ -0,0 +1,27 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/bluetooth/resources/bluetooth-test.js +// META: script=/bluetooth/resources/bluetooth-fake-devices.js + +'use strict'; + +let iframe = document.createElement('iframe'); + +bluetooth_test(async () => { + await getConnectedHealthThermometerDevice(); + await new Promise(resolve => { + iframe.src = '/bluetooth/resources/health-thermometer-iframe.html'; + iframe.sandbox.add('allow-scripts'); + iframe.allow = 'bluetooth'; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve); + }); + await new Promise(resolve => { + iframe.contentWindow.postMessage({type: 'RequestLEScan'}, '*'); + + window.addEventListener('message', (messageEvent) => { + assert_false(/^FAIL: .*/.test(messageEvent.data)); + resolve(); + }); + }); +}, 'Calls to Bluetooth APIs from a sandboxed iframe are valid.'); \ No newline at end of file diff --git a/bluetooth/resources/health-thermometer-iframe.html b/bluetooth/resources/health-thermometer-iframe.html index 78774b4d158f6a..f9f7a6f0d7d9cf 100644 --- a/bluetooth/resources/health-thermometer-iframe.html +++ b/bluetooth/resources/health-thermometer-iframe.html @@ -17,13 +17,20 @@ window.addEventListener('message', (messageEvent) => { switch (messageEvent.data.type) { case 'GetAvailability': - navigator.bluetooth.getAvailability().then( - availability => parent.postMessage(availability, '*')); + navigator.bluetooth.getAvailability() + .then(availability => parent.postMessage(availability, '*')) + .catch(err => parent.postMessage(`FAIL: ${err}`, '*')); + break; + case 'GetDevices': + navigator.bluetooth.getDevices() + .then(devices => parent.postMessage('Success', '*')) + .catch(err => parent.postMessage(`FAIL: ${err}`, '*')); break; case 'RequestDevice': - test_driver.click(document.getElementsByTagName("button")[0]) - .then(() => navigator.bluetooth - .requestDevice({filters: [{services: ['generic_access']}]})) + test_driver.click(document.getElementsByTagName('button')[0]) + .then( + () => navigator.bluetooth.requestDevice( + {filters: [{services: ['generic_access']}]})) .then(device => { if (device.constructor.name === 'BluetoothDevice') { parent.postMessage('Success', '*'); @@ -32,7 +39,22 @@ `FAIL: requestDevice in iframe returned ${device.name}`, '*'); } }) - .catch(err => parent.postMessage(`${err.name}: ${err.message}`, '*')); + .catch(err => parent.postMessage(`FAIL: ${err.name}: ${err.message}`, '*')); + break; + case 'RequestLEScan': + test_driver.click(document.getElementsByTagName('button')[0]) + .then( + () => navigator.bluetooth.requestLEScan( + {filters: [{name: 'Health Thermometer'}]})) + .then(leScan => { + if (leScan.active) { + parent.postMessage('Success', '*'); + leScan.stop(); + } else { + parent.postMessage(`FAIL: the LE scan hasn't been initiated.`, '*'); + } + }) + .catch(err => parent.postMessage(`FAIL: ${err.name}: ${err.message}`, '*')); break; case 'RequestAndConnect': requestDeviceWithOptionsAndConnect(messageEvent.data.options)