diff --git a/internal/web/security.go b/internal/web/security.go index bb35cbe..28b93b9 100644 --- a/internal/web/security.go +++ b/internal/web/security.go @@ -70,12 +70,18 @@ func (e *Env) formAction(r *http.Request) string { } // permissionsPolicy denies powerful browser features we don't use and explicitly -// allows the two we do: WebSerial (the KISS modem on the confirm/console pages) -// and WebAuthn (passkeys). Unknown tokens are ignored by browsers that don't -// implement them. -const permissionsPolicy = "serial=(self), " + +// allows the three we do: WebSerial and WebUSB (both reach the KISS modem on the +// confirm/console pages — see serial-port.js, which falls back to WebUSB where +// WebSerial is absent, notably Android) and WebAuthn (passkeys). Unknown tokens +// are ignored by browsers that don't implement them. +// +// usb was denied here until WebUSB became a real transport, and flipping it back +// to () would silently break Android: the failure is a SecurityError from +// requestDevice, not a CSP violation, so neither the CSP report endpoint nor the +// e2e harness would notice. +const permissionsPolicy = "serial=(self), usb=(self), " + "publickey-credentials-get=(self), publickey-credentials-create=(self), " + - "geolocation=(), camera=(), microphone=(), payment=(), usb=()" + "geolocation=(), camera=(), microphone=(), payment=()" // securityHeaders sets a strict Content-Security-Policy (with a fresh per-request // script nonce, also exposed via the context for templates) plus companion diff --git a/internal/web/security_test.go b/internal/web/security_test.go index 475ab53..f74db6f 100644 --- a/internal/web/security_test.go +++ b/internal/web/security_test.go @@ -44,8 +44,12 @@ func TestSecurityHeadersCSPNonce(t *testing.T) { } // Permissions-Policy must keep the features we actually use. pp := rec1.Header().Get("Permissions-Policy") - if !strings.Contains(pp, "serial=(self)") || !strings.Contains(pp, "publickey-credentials-get=(self)") { - t.Errorf("Permissions-Policy doesn't allow serial/webauthn: %q", pp) + // usb=(self) matters as much as serial: it is the only transport that reaches + // a device on Android, and denying it fails as a SecurityError from + // requestDevice — invisible to the CSP report endpoint and the e2e harness. + if !strings.Contains(pp, "serial=(self)") || !strings.Contains(pp, "usb=(self)") || + !strings.Contains(pp, "publickey-credentials-get=(self)") { + t.Errorf("Permissions-Policy doesn't allow serial/usb/webauthn: %q", pp) } if seen[0] == "" || seen[1] == "" || seen[0] == seen[1] { t.Errorf("nonce not fresh per request: %q, %q", seen[0], seen[1])