From 3aaee0751cf44050fd84f0e02cb54d7de351db31 Mon Sep 17 00:00:00 2001 From: Alvin Ji Date: Wed, 3 Jun 2026 20:48:27 -0700 Subject: [PATCH 1/3] Restrict control transfers to standard requests and protected classes This change aligns the specification with recent security improvements in Chromium. It introduces an allowlist for standard control requests and ensures that transfers targeting protected interface classes are blocked, even when using non-interface recipients. --- index.bs | 50 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/index.bs b/index.bs index d114136..753e4be 100644 --- a/index.bs +++ b/index.bs @@ -1476,8 +1476,8 @@ method, when invoked, MUST run the following steps: 1. Let |global| be [=this=]'s [=relevant global object=]. 1. If [=check if the device is configured=] with [=this=] returns a {{Promise}}, return that value. -1. If [=check the validity of the control transfer parameters=] with [=this=] - and |setup| returns a {{Promise}}, return that value. +1. If [=check the validity of the control transfer parameters=] with [=this=], + |setup|, and {{"in"}} returns a {{Promise}}, return that value. 1. Let |promise| be [=a new promise=]. 1. Run the following steps [=in parallel=]. 1. If |length| is greater than 0, let |buffer| be a host buffer with space @@ -1515,8 +1515,8 @@ method, when invoked, MUST run the following steps: 1. Let |global| be [=this=]'s [=relevant global object=]. 1. If [=check if the device is configured=] with [=this=] returns a {{Promise}}, return that value. -1. If [=check the validity of the control transfer parameters=] with [=this=] - and |setup| returns a {{Promise}}, return that value. +1. If [=check the validity of the control transfer parameters=] with [=this=], + |setup|, and {{"out"}} returns a {{Promise}}, return that value. 1. [=Get a copy of the buffer source=] |data| and let the result be |bytes|. 1. Let |promise| be [=a new promise=]. 1. Run the following steps [=in parallel=]. @@ -1797,34 +1797,52 @@ Issue(36): What configuration is the device in after it resets?
To check the validity of the control transfer parameters with the -given {{USBDevice}} |device| and {{USBControlTransferParameters}} |setup|, -perform the following steps: +given {{USBDevice}} |device|, {{USBControlTransferParameters}} |setup|, and +{{USBDirection}} |direction|, perform the following steps: 1. Let |configuration| be the result of [=finding the current configuration=] with |device|. 1. If |configuration| is `null`, return `undefined`. -1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"interface"}}, +1. If |setup|.{{USBControlTransferParameters/requestType}} is {{"standard"}}, + perform the following steps: + 1. If |direction| is not {{"in"}}, return [=a promise rejected with=] a + "{{SecurityError}}" {{DOMException}}. + 1. If |setup|.{{USBControlTransferParameters/request}} is not one of + `0x00` (GET_STATUS), `0x06` (GET_DESCRIPTOR), `0x08` (GET_CONFIGURATION), + `0x0A` (GET_INTERFACE) or `0x0C` (SYNCH_FRAME), return [=a promise + rejected with=] a "{{SecurityError}}" {{DOMException}}. +1. Let |interface| be `null`. +1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"interface"}} or + |setup|.{{USBControlTransferParameters/requestType}} is {{"class"}}, perform the following steps: 1. Let |interfaceNumber| be the lower 8 bits of |setup|.{{USBControlTransferParameters/index}}. 1. Let |interfaceIndex| be the result of [=finding the interface index=] with |interfaceNumber| and |configuration|. - 1. If |interfaceIndex| is equal to `-1`, return [=a promise rejected with=] - a "{{NotFoundError}}" {{DOMException}}. - 1. Let |interface| be + 1. If |interfaceIndex| is not `-1`, set |interface| to |configuration|.{{USBConfiguration/[[interfaces]]}}[|interfaceIndex|]. +1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"endpoint"}}, + perform the following steps: + 1. Let |endpointAddress| be |setup|.{{USBControlTransferParameters/index}}. + 1. Let |endpoint| be the result of [=finding the endpoint=] with + |endpointAddress| and |device|. + 1. If |endpoint| is not `null`, perform the following steps: + 1. Let |alternate| be |endpoint|.{{USBEndpoint/[[alternateInterface]]}}. + 1. Set |interface| to |alternate|.{{USBAlternateInterface/[[interface]]}}. +1. If |interface| is not `null` and + |interface|.{{USBInterface/[[isProtectedClass]]}} is `true`, return [=a + promise rejected with=] a "{{SecurityError}}" {{DOMException}}. +1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"interface"}}, + perform the following steps: + 1. If |interface| is `null`, return [=a promise rejected with=] a + "{{NotFoundError}}" {{DOMException}}. 1. If the result of [=finding if the interface is claimed=] with |interface| is not `true`, return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. 1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"endpoint"}}, run the following steps: - 1. Let |endpointAddress| be |setup|.{{USBControlTransferParameters/index}}. - 1. Let |endpoint| be the result of [=finding the endpoint=] with - |endpointAddress| and |device|. - 1. If |endpoint| is `null`, return [=a promise rejected with=] a + 1. If |interface| is `null`, return [=a promise rejected with=] a "{{NotFoundError}}" {{DOMException}}. - 1. Let |alternate| be |endpoint|.{{USBEndpoint/[[alternateInterface]]}}. - 1. Let |interface| be |alternate|.{{USBAlternateInterface/[[interface]]}}. 1. If the result of [=finding if the interface is claimed=] with |interface| is `false`, return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. From eb40e2599445fa0d3b548ed52dbba665c95fdfd7 Mon Sep 17 00:00:00 2001 From: Alvin Ji Date: Thu, 4 Jun 2026 11:48:15 -0700 Subject: [PATCH 2/3] Restructure control transfer validation algorithm for clarity --- index.bs | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/index.bs b/index.bs index 753e4be..30f7ebd 100644 --- a/index.bs +++ b/index.bs @@ -1805,44 +1805,50 @@ given {{USBDevice}} |device|, {{USBControlTransferParameters}} |setup|, and 1. If |configuration| is `null`, return `undefined`. 1. If |setup|.{{USBControlTransferParameters/requestType}} is {{"standard"}}, perform the following steps: - 1. If |direction| is not {{"in"}}, return [=a promise rejected with=] a + 1. If |direction| is {{"out"}}, return [=a promise rejected with=] a "{{SecurityError}}" {{DOMException}}. 1. If |setup|.{{USBControlTransferParameters/request}} is not one of `0x00` (GET_STATUS), `0x06` (GET_DESCRIPTOR), `0x08` (GET_CONFIGURATION), `0x0A` (GET_INTERFACE) or `0x0C` (SYNCH_FRAME), return [=a promise rejected with=] a "{{SecurityError}}" {{DOMException}}. -1. Let |interface| be `null`. -1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"interface"}} or - |setup|.{{USBControlTransferParameters/requestType}} is {{"class"}}, +1. If |setup|.{{USBControlTransferParameters/requestType}} is {{"class"}}, perform the following steps: 1. Let |interfaceNumber| be the lower 8 bits of |setup|.{{USBControlTransferParameters/index}}. 1. Let |interfaceIndex| be the result of [=finding the interface index=] with |interfaceNumber| and |configuration|. - 1. If |interfaceIndex| is not `-1`, set |interface| to - |configuration|.{{USBConfiguration/[[interfaces]]}}[|interfaceIndex|]. -1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"endpoint"}}, - perform the following steps: - 1. Let |endpointAddress| be |setup|.{{USBControlTransferParameters/index}}. - 1. Let |endpoint| be the result of [=finding the endpoint=] with - |endpointAddress| and |device|. - 1. If |endpoint| is not `null`, perform the following steps: - 1. Let |alternate| be |endpoint|.{{USBEndpoint/[[alternateInterface]]}}. - 1. Set |interface| to |alternate|.{{USBAlternateInterface/[[interface]]}}. -1. If |interface| is not `null` and - |interface|.{{USBInterface/[[isProtectedClass]]}} is `true`, return [=a - promise rejected with=] a "{{SecurityError}}" {{DOMException}}. + 1. If |interfaceIndex| is not `-1`, perform the following steps: + 1. Let |interface| be + |configuration|.{{USBConfiguration/[[interfaces]]}}[|interfaceIndex|]. + 1. If |interface|.{{USBInterface/[[isProtectedClass]]}} is `true`, + return [=a promise rejected with=] a "{{SecurityError}}" + {{DOMException}}. 1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"interface"}}, perform the following steps: - 1. If |interface| is `null`, return [=a promise rejected with=] a + 1. Let |interfaceNumber| be the lower 8 bits of + |setup|.{{USBControlTransferParameters/index}}. + 1. Let |interfaceIndex| be the result of [=finding the interface index=] + with |interfaceNumber| and |configuration|. + 1. If |interfaceIndex| is `-1`, return [=a promise rejected with=] a "{{NotFoundError}}" {{DOMException}}. + 1. Let |interface| be + |configuration|.{{USBConfiguration/[[interfaces]]}}[|interfaceIndex|]. + 1. If |interface|.{{USBInterface/[[isProtectedClass]]}} is `true`, return [=a + promise rejected with=] a "{{SecurityError}}" {{DOMException}}. 1. If the result of [=finding if the interface is claimed=] with |interface| is not `true`, return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. -1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"endpoint"}}, run - the following steps: - 1. If |interface| is `null`, return [=a promise rejected with=] a +1. If |setup|.{{USBControlTransferParameters/recipient}} is {{"endpoint"}}, + perform the following steps: + 1. Let |endpointAddress| be |setup|.{{USBControlTransferParameters/index}}. + 1. Let |endpoint| be the result of [=finding the endpoint=] with + |endpointAddress| and |device|. + 1. If |endpoint| is `null`, return [=a promise rejected with=] a "{{NotFoundError}}" {{DOMException}}. + 1. Let |alternate| be |endpoint|.{{USBEndpoint/[[alternateInterface]]}}. + 1. Let |interface| be |alternate|.{{USBAlternateInterface/[[interface]]}}. + 1. If |interface|.{{USBInterface/[[isProtectedClass]]}} is `true`, return [=a + promise rejected with=] a "{{SecurityError}}" {{DOMException}}. 1. If the result of [=finding if the interface is claimed=] with |interface| is `false`, return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. @@ -2817,6 +2823,7 @@ spec: html spec: webidl type: dfn text: resolve +spec: permissions; type: enum-value; text: prompt spec:infra; type:dfn; text:list spec:infra; type:dfn; for:list; text:for each spec:infra; type:dfn; for:list; text:append From ef9af363f72908bd89b979c6e2ab9ae5bb353a13 Mon Sep 17 00:00:00 2001 From: Alvin Ji Date: Thu, 4 Jun 2026 13:08:16 -0700 Subject: [PATCH 3/3] Remove prompt link-default (warning is acceptable) --- index.bs | 1 - 1 file changed, 1 deletion(-) diff --git a/index.bs b/index.bs index 30f7ebd..398a97d 100644 --- a/index.bs +++ b/index.bs @@ -2823,7 +2823,6 @@ spec: html spec: webidl type: dfn text: resolve -spec: permissions; type: enum-value; text: prompt spec:infra; type:dfn; text:list spec:infra; type:dfn; for:list; text:for each spec:infra; type:dfn; for:list; text:append