Skip to content

Commit

Permalink
Reject Web Bluetooth requests with an opaque origin
Browse files Browse the repository at this point in the history
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 <reillyg@chromium.org>
Commit-Queue: Sina Firoozabadi <sinafirooz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1089042}
  • Loading branch information
gsinafirooz authored and chromium-wpt-export-bot committed Jan 5, 2023
1 parent 21ae69f commit 61e57be
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 8 deletions.
13 changes: 13 additions & 0 deletions bluetooth/getAvailability/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
<!DOCTYPE html>

<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';

promise_test(async (t) => {
await promise_rejects_dom(
t, 'SecurityError', navigator.bluetooth.getAvailability(),
'getAvailability() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
}, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
</script>
@@ -0,0 +1 @@
Content-Security-Policy: sandbox allow-scripts
27 changes: 27 additions & 0 deletions 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.');
13 changes: 13 additions & 0 deletions bluetooth/getDevices/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
<!DOCTYPE html>

<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';

promise_test(async (t) => {
await promise_rejects_dom(
t, 'SecurityError', navigator.bluetooth.getDevices(),
'getDevices() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
}, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
</script>
@@ -0,0 +1 @@
Content-Security-Policy: sandbox allow-scripts
27 changes: 27 additions & 0 deletions 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.');
Expand Up @@ -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);
13 changes: 13 additions & 0 deletions bluetooth/requestDevice/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
<!DOCTYPE html>

<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';

promise_test(async (t) => {
await promise_rejects_dom(
t, 'SecurityError', navigator.bluetooth.requestDevice(),
'requestDevice() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
}, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
</script>
@@ -0,0 +1 @@
Content-Security-Policy: sandbox allow-scripts
Expand Up @@ -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.';

Expand Down
27 changes: 27 additions & 0 deletions 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.');
13 changes: 13 additions & 0 deletions bluetooth/requestLEScan/reject_opaque_origin.https.html
@@ -0,0 +1,13 @@
<!DOCTYPE html>

<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';

promise_test(async (t) => {
await promise_rejects_dom(
t, 'SecurityError', navigator.bluetooth.requestLEScan(),
'requestLEScan() should throw a SecurityError DOMException when called from a context where the top-level document has an opaque origin.');
}, 'Calls to Bluetooth APIs from an origin with opaque top origin get blocked.');
</script>
@@ -0,0 +1 @@
Content-Security-Policy: sandbox allow-scripts
27 changes: 27 additions & 0 deletions 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.');
34 changes: 28 additions & 6 deletions bluetooth/resources/health-thermometer-iframe.html
Expand Up @@ -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', '*');
Expand All @@ -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)
Expand Down

0 comments on commit 61e57be

Please sign in to comment.