Disable WebAuthn in CEF OSR panes
- commit
- 27813e6347404700a2d0d25cb969feecf37b3abb
- author
- brice · · 2026-04-29 17:23 UTC
- parent
- 4613d9c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +221 -6
M docs/reference/permissions.md +8 -0
| @@ -23,6 +23,14 @@ These permissions require explicit user approval: | |||
| 23 | 23 | ||
| 24 | 24 | When both microphone and camera are requested together, a combined dialog is shown. | |
| 25 | 25 | ||
| 26 | + | ## WebAuthn / passkeys / security keys | |
| 27 | + | ||
| 28 | + | Dumber currently **disables** WebAuthn (`PublicKeyCredential`, passkeys, FIDO2/security keys) in embedded CEF panes. The current CEF integration uses windowless/OSR Alloy browsers and does not expose the Chromium WebAuthn request UI/delegate needed for PIN, account selection, or transport UI. | |
| 29 | + | ||
| 30 | + | If a site requires passkeys or hardware security keys, use the system browser for that auth flow until a dedicated Chrome-runtime auth-window prototype exists. | |
| 31 | + | ||
| 32 | + | > ⚠️ **Developer override (unsupported)**: Set `DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE=1` to temporarily re-enable WebAuthn. This is unsupported and may crash. | |
| 33 | + | ||
| 26 | 34 | ## Permission Dialog | |
| 27 | 35 | ||
| 28 | 36 | When a site requests mic/camera access, a dialog appears with four options: | |
| @@ -23,6 +23,14 @@ These permissions require explicit user approval: | |||||
| 23 | 23 | ||||
| 24 | When both microphone and camera are requested together, a combined dialog is shown. | 24 | When both microphone and camera are requested together, a combined dialog is shown. | ||
| 25 | 25 | ||||
| 26 | + | ## WebAuthn / passkeys / security keys | |||
| 27 | + | ||||
| 28 | + | Dumber currently **disables** WebAuthn (`PublicKeyCredential`, passkeys, FIDO2/security keys) in embedded CEF panes. The current CEF integration uses windowless/OSR Alloy browsers and does not expose the Chromium WebAuthn request UI/delegate needed for PIN, account selection, or transport UI. | |||
| 29 | + | ||||
| 30 | + | If a site requires passkeys or hardware security keys, use the system browser for that auth flow until a dedicated Chrome-runtime auth-window prototype exists. | |||
| 31 | + | ||||
| 32 | + | > ⚠️ **Developer override (unsupported)**: Set `DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE=1` to temporarily re-enable WebAuthn. This is unsupported and may crash. | |||
| 33 | + | ||||
| 26 | ## Permission Dialog | 34 | ## Permission Dialog | ||
| 27 | 35 | ||||
| 28 | When a site requests mic/camera access, a dialog appears with four options: | 36 | When a site requests mic/camera access, a dialog appears with four options: | ||
M internal/infrastructure/cef/app.go +72 -1
| @@ -8,7 +8,18 @@ import ( | |||
| 8 | 8 | "github.com/bnema/dumber/internal/logging" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | - | const dumbSchemeName = "dumb" | |
| 11 | + | const ( | |
| 12 | + | dumbSchemeName = "dumb" | |
| 13 | + | chromiumDisableFeaturesSwitch = "disable-features" | |
| 14 | + | chromiumDisableBlinkFeaturesSwitch = "disable-blink-features" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // Chromium's core Blink runtime flag for the Web Authentication API is | |
| 18 | + | // "WebAuth" (PublicKeyCredential IDL uses RuntimeEnabled=WebAuth). The | |
| 19 | + | // longer "WebAuthentication*" names are separate subfeatures/metrics. | |
| 20 | + | var cefWebAuthnFeaturesDisabledByPolicy = []string{ | |
| 21 | + | "WebAuth", | |
| 22 | + | } | |
| 12 | 23 | ||
| 13 | 24 | func dumbSchemeOptions() int32 { | |
| 14 | 25 | return purecef.SchemeOptionsSchemeOptionStandard | | |
| @@ -38,6 +49,52 @@ func configureCommandLine(commandLine purecef.CommandLine) { | |||
| 38 | 49 | // Reddit autoplay muted videos in the feed; without this policy | |
| 39 | 50 | // Chromium blocks them, showing an infinite spinner. | |
| 40 | 51 | commandLine.AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required") | |
| 52 | + | ||
| 53 | + | configureWebAuthnFeaturePolicy(commandLine) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func configureWebAuthnFeaturePolicy(commandLine purecef.CommandLine) { | |
| 57 | + | if commandLine == nil || cefWebAuthnUnsafeEnabled() { | |
| 58 | + | return | |
| 59 | + | } | |
| 60 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...) | |
| 61 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) { | |
| 65 | + | if commandLine == nil || len(values) == 0 { | |
| 66 | + | return | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | seen := make(map[string]struct{}, len(values)) | |
| 70 | + | combined := make([]string, 0, len(values)) | |
| 71 | + | for _, value := range strings.Split(commandLine.GetSwitchValue(name), ",") { | |
| 72 | + | value = strings.TrimSpace(value) | |
| 73 | + | if value == "" { | |
| 74 | + | continue | |
| 75 | + | } | |
| 76 | + | if _, ok := seen[value]; ok { | |
| 77 | + | continue | |
| 78 | + | } | |
| 79 | + | seen[value] = struct{}{} | |
| 80 | + | combined = append(combined, value) | |
| 81 | + | } | |
| 82 | + | for _, value := range values { | |
| 83 | + | value = strings.TrimSpace(value) | |
| 84 | + | if value == "" { | |
| 85 | + | continue | |
| 86 | + | } | |
| 87 | + | if _, ok := seen[value]; ok { | |
| 88 | + | continue | |
| 89 | + | } | |
| 90 | + | seen[value] = struct{}{} | |
| 91 | + | combined = append(combined, value) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | if len(combined) == 0 { | |
| 95 | + | return | |
| 96 | + | } | |
| 97 | + | commandLine.AppendSwitchWithValue(name, strings.Join(combined, ",")) | |
| 41 | 98 | } | |
| 42 | 99 | ||
| 43 | 100 | // dumberApp implements purecef.App to provide custom scheme registration, | |
| @@ -74,6 +131,19 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin | |||
| 74 | 131 | if commandLine != nil { | |
| 75 | 132 | configureCommandLine(commandLine) | |
| 76 | 133 | ||
| 134 | + | if processType == "" { | |
| 135 | + | if cefWebAuthnUnsafeEnabled() { | |
| 136 | + | log.Warn(). | |
| 137 | + | Str("env_var", cefEnableWebAuthnUnsafeEnvVar). | |
| 138 | + | Msg("cef: WebAuthn enabled via unsupported developer override") | |
| 139 | + | } else { | |
| 140 | + | log.Info(). | |
| 141 | + | Strs("disabled_features", cefWebAuthnFeaturesDisabledByPolicy). | |
| 142 | + | Str("override_env_var", cefEnableWebAuthnUnsafeEnvVar). | |
| 143 | + | Msg("cef: WebAuthn disabled for CEF windowless/Alloy runtime") | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | ||
| 77 | 147 | cmdline := commandLine.GetCommandLineString() | |
| 78 | 148 | if len(cmdline) > maxCmdLineLogLen { | |
| 79 | 149 | runes := []rune(cmdline) | |
| @@ -124,6 +194,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine) | |||
| 124 | 194 | useAngle := "" | |
| 125 | 195 | ozonePlatform := "" | |
| 126 | 196 | if commandLine != nil { | |
| 197 | + | configureWebAuthnFeaturePolicy(commandLine) | |
| 127 | 198 | appendSwitchIfMissing(commandLine, "no-zygote") | |
| 128 | 199 | processType = commandLine.GetSwitchValue("type") | |
| 129 | 200 | commandLineString = commandLine.GetCommandLineString() | |
| @@ -8,7 +8,18 @@ import ( | |||||
| 8 | "github.com/bnema/dumber/internal/logging" | 8 | "github.com/bnema/dumber/internal/logging" | ||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | - | const dumbSchemeName = "dumb" | 11 | + | const ( |
| 12 | + | dumbSchemeName = "dumb" | |||
| 13 | + | chromiumDisableFeaturesSwitch = "disable-features" | |||
| 14 | + | chromiumDisableBlinkFeaturesSwitch = "disable-blink-features" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // Chromium's core Blink runtime flag for the Web Authentication API is | |||
| 18 | + | // "WebAuth" (PublicKeyCredential IDL uses RuntimeEnabled=WebAuth). The | |||
| 19 | + | // longer "WebAuthentication*" names are separate subfeatures/metrics. | |||
| 20 | + | var cefWebAuthnFeaturesDisabledByPolicy = []string{ | |||
| 21 | + | "WebAuth", | |||
| 22 | + | } | |||
| 12 | 23 | ||||
| 13 | func dumbSchemeOptions() int32 { | 24 | func dumbSchemeOptions() int32 { | ||
| 14 | return purecef.SchemeOptionsSchemeOptionStandard | | 25 | return purecef.SchemeOptionsSchemeOptionStandard | | ||
| @@ -38,6 +49,52 @@ func configureCommandLine(commandLine purecef.CommandLine) { | |||||
| 38 | // Reddit autoplay muted videos in the feed; without this policy | 49 | // Reddit autoplay muted videos in the feed; without this policy | ||
| 39 | // Chromium blocks them, showing an infinite spinner. | 50 | // Chromium blocks them, showing an infinite spinner. | ||
| 40 | commandLine.AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required") | 51 | commandLine.AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required") | ||
| 52 | + | ||||
| 53 | + | configureWebAuthnFeaturePolicy(commandLine) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | func configureWebAuthnFeaturePolicy(commandLine purecef.CommandLine) { | |||
| 57 | + | if commandLine == nil || cefWebAuthnUnsafeEnabled() { | |||
| 58 | + | return | |||
| 59 | + | } | |||
| 60 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...) | |||
| 61 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) { | |||
| 65 | + | if commandLine == nil || len(values) == 0 { | |||
| 66 | + | return | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | seen := make(map[string]struct{}, len(values)) | |||
| 70 | + | combined := make([]string, 0, len(values)) | |||
| 71 | + | for _, value := range strings.Split(commandLine.GetSwitchValue(name), ",") { | |||
| 72 | + | value = strings.TrimSpace(value) | |||
| 73 | + | if value == "" { | |||
| 74 | + | continue | |||
| 75 | + | } | |||
| 76 | + | if _, ok := seen[value]; ok { | |||
| 77 | + | continue | |||
| 78 | + | } | |||
| 79 | + | seen[value] = struct{}{} | |||
| 80 | + | combined = append(combined, value) | |||
| 81 | + | } | |||
| 82 | + | for _, value := range values { | |||
| 83 | + | value = strings.TrimSpace(value) | |||
| 84 | + | if value == "" { | |||
| 85 | + | continue | |||
| 86 | + | } | |||
| 87 | + | if _, ok := seen[value]; ok { | |||
| 88 | + | continue | |||
| 89 | + | } | |||
| 90 | + | seen[value] = struct{}{} | |||
| 91 | + | combined = append(combined, value) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | if len(combined) == 0 { | |||
| 95 | + | return | |||
| 96 | + | } | |||
| 97 | + | commandLine.AppendSwitchWithValue(name, strings.Join(combined, ",")) | |||
| 41 | } | 98 | } | ||
| 42 | 99 | ||||
| 43 | // dumberApp implements purecef.App to provide custom scheme registration, | 100 | // dumberApp implements purecef.App to provide custom scheme registration, | ||
| @@ -74,6 +131,19 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin | |||||
| 74 | if commandLine != nil { | 131 | if commandLine != nil { | ||
| 75 | configureCommandLine(commandLine) | 132 | configureCommandLine(commandLine) | ||
| 76 | 133 | ||||
| 134 | + | if processType == "" { | |||
| 135 | + | if cefWebAuthnUnsafeEnabled() { | |||
| 136 | + | log.Warn(). | |||
| 137 | + | Str("env_var", cefEnableWebAuthnUnsafeEnvVar). | |||
| 138 | + | Msg("cef: WebAuthn enabled via unsupported developer override") | |||
| 139 | + | } else { | |||
| 140 | + | log.Info(). | |||
| 141 | + | Strs("disabled_features", cefWebAuthnFeaturesDisabledByPolicy). | |||
| 142 | + | Str("override_env_var", cefEnableWebAuthnUnsafeEnvVar). | |||
| 143 | + | Msg("cef: WebAuthn disabled for CEF windowless/Alloy runtime") | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | ||||
| 77 | cmdline := commandLine.GetCommandLineString() | 147 | cmdline := commandLine.GetCommandLineString() | ||
| 78 | if len(cmdline) > maxCmdLineLogLen { | 148 | if len(cmdline) > maxCmdLineLogLen { | ||
| 79 | runes := []rune(cmdline) | 149 | runes := []rune(cmdline) | ||
| @@ -124,6 +194,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine) | |||||
| 124 | useAngle := "" | 194 | useAngle := "" | ||
| 125 | ozonePlatform := "" | 195 | ozonePlatform := "" | ||
| 126 | if commandLine != nil { | 196 | if commandLine != nil { | ||
| 197 | + | configureWebAuthnFeaturePolicy(commandLine) | |||
| 127 | appendSwitchIfMissing(commandLine, "no-zygote") | 198 | appendSwitchIfMissing(commandLine, "no-zygote") | ||
| 128 | processType = commandLine.GetSwitchValue("type") | 199 | processType = commandLine.GetSwitchValue("type") | ||
| 129 | commandLineString = commandLine.GetCommandLineString() | 200 | commandLineString = commandLine.GetCommandLineString() | ||
M internal/infrastructure/cef/app_test.go +102 -4
| @@ -2,6 +2,7 @@ package cef | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "strings" | |
| 5 | 6 | "testing" | |
| 6 | 7 | "unsafe" | |
| 7 | 8 | ||
| @@ -175,8 +176,6 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testin | |||
| 175 | 176 | } | |
| 176 | 177 | ||
| 177 | 178 | func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) { | |
| 178 | - | t.Parallel() | |
| 179 | - | ||
| 180 | 179 | commandLine := newMutableCommandLineStub() | |
| 181 | 180 | configureCommandLine(commandLine) | |
| 182 | 181 | ||
| @@ -188,9 +187,90 @@ func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) { | |||
| 188 | 187 | } | |
| 189 | 188 | } | |
| 190 | 189 | ||
| 191 | - | func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | |
| 192 | - | t.Parallel() | |
| 190 | + | func TestConfigureCommandLine_DisablesWebAuthnByDefault(t *testing.T) { | |
| 191 | + | commandLine := newMutableCommandLineStub() | |
| 192 | + | configureCommandLine(commandLine) | |
| 193 | + | ||
| 194 | + | want := "WebAuth" | |
| 195 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want { | |
| 196 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |
| 197 | + | } | |
| 198 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want { | |
| 199 | + | t.Fatalf("disable-blink-features = %q, want %q", got, want) | |
| 200 | + | } | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | func TestConfigureCommandLine_WebAuthnUnsafeOptInDoesNotDisableWebAuth(t *testing.T) { | |
| 204 | + | t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1") | |
| 193 | 205 | ||
| 206 | + | commandLine := newMutableCommandLineStub() | |
| 207 | + | configureCommandLine(commandLine) | |
| 208 | + | ||
| 209 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); strings.Contains(got, "WebAuth") { | |
| 210 | + | t.Fatalf("disable-features = %q, should not contain WebAuth", got) | |
| 211 | + | } | |
| 212 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); strings.Contains(got, "WebAuth") { | |
| 213 | + | t.Fatalf("disable-blink-features = %q, should not contain WebAuth", got) | |
| 214 | + | } | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | func TestConfigureCommandLine_WebAuthnDisablePreservesExistingDisableFeatures(t *testing.T) { | |
| 218 | + | commandLine := newMutableCommandLineStub() | |
| 219 | + | commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth") | |
| 220 | + | commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "ExistingBlinkFeature,WebAuth") | |
| 221 | + | ||
| 222 | + | configureCommandLine(commandLine) | |
| 223 | + | ||
| 224 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,WebAuth" { | |
| 225 | + | t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,WebAuth") | |
| 226 | + | } | |
| 227 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature,WebAuth" { | |
| 228 | + | t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature,WebAuth") | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func TestAppendUniqueCommaSeparatedSwitchValues(t *testing.T) { | |
| 233 | + | t.Run("trims whitespace and skips empty existing values", func(t *testing.T) { | |
| 234 | + | commandLine := newMutableCommandLineStub() | |
| 235 | + | commandLine.AppendSwitchWithValue("disable-features", " , ExistingFeature , ") | |
| 236 | + | ||
| 237 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA") | |
| 238 | + | ||
| 239 | + | want := "ExistingFeature,FeatureA" | |
| 240 | + | if got := commandLine.GetSwitchValue("disable-features"); got != want { | |
| 241 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |
| 242 | + | } | |
| 243 | + | }) | |
| 244 | + | ||
| 245 | + | t.Run("deduplicates existing and appended values", func(t *testing.T) { | |
| 246 | + | commandLine := newMutableCommandLineStub() | |
| 247 | + | commandLine.AppendSwitchWithValue("disable-features", "FeatureA,FeatureA") | |
| 248 | + | ||
| 249 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA", "FeatureB") | |
| 250 | + | ||
| 251 | + | want := "FeatureA,FeatureB" | |
| 252 | + | if got := commandLine.GetSwitchValue("disable-features"); got != want { | |
| 253 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |
| 254 | + | } | |
| 255 | + | }) | |
| 256 | + | ||
| 257 | + | t.Run("empty appended values are no-op", func(t *testing.T) { | |
| 258 | + | commandLine := newMutableCommandLineStub() | |
| 259 | + | commandLine.AppendSwitchWithValue("disable-features", "ExistingFeature") | |
| 260 | + | ||
| 261 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features") | |
| 262 | + | ||
| 263 | + | if got := commandLine.GetSwitchValue("disable-features"); got != "ExistingFeature" { | |
| 264 | + | t.Fatalf("disable-features = %q, want ExistingFeature", got) | |
| 265 | + | } | |
| 266 | + | }) | |
| 267 | + | ||
| 268 | + | t.Run("nil command line is no-op", func(_ *testing.T) { | |
| 269 | + | appendUniqueCommaSeparatedSwitchValues(nil, "disable-features", "FeatureA") | |
| 270 | + | }) | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | |
| 194 | 274 | commandLine := newMutableCommandLineStub() | |
| 195 | 275 | commandLine.AppendSwitchWithValue("type", "renderer") | |
| 196 | 276 | ||
| @@ -200,3 +280,21 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | |||
| 200 | 280 | t.Fatal("expected no-zygote switch to be appended for child processes") | |
| 201 | 281 | } | |
| 202 | 282 | } | |
| 283 | + | ||
| 284 | + | func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) { | |
| 285 | + | commandLine := newMutableCommandLineStub() | |
| 286 | + | commandLine.AppendSwitchWithValue("type", "renderer") | |
| 287 | + | ||
| 288 | + | (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine) | |
| 289 | + | ||
| 290 | + | want := "WebAuth" | |
| 291 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want { | |
| 292 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |
| 293 | + | } | |
| 294 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want { | |
| 295 | + | t.Fatalf("disable-blink-features = %q, want %q", got, want) | |
| 296 | + | } | |
| 297 | + | if !commandLine.HasSwitch("no-zygote") { | |
| 298 | + | t.Fatal("expected no-zygote switch to be appended") | |
| 299 | + | } | |
| 300 | + | } | |
| @@ -2,6 +2,7 @@ package cef | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "strings" | |||
| 5 | "testing" | 6 | "testing" | ||
| 6 | "unsafe" | 7 | "unsafe" | ||
| 7 | 8 | ||||
| @@ -175,8 +176,6 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testin | |||||
| 175 | } | 176 | } | ||
| 176 | 177 | ||||
| 177 | func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) { | 178 | func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) { | ||
| 178 | - | t.Parallel() | |||
| 179 | - | ||||
| 180 | commandLine := newMutableCommandLineStub() | 179 | commandLine := newMutableCommandLineStub() | ||
| 181 | configureCommandLine(commandLine) | 180 | configureCommandLine(commandLine) | ||
| 182 | 181 | ||||
| @@ -188,9 +187,90 @@ func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) { | |||||
| 188 | } | 187 | } | ||
| 189 | } | 188 | } | ||
| 190 | 189 | ||||
| 191 | - | func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | 190 | + | func TestConfigureCommandLine_DisablesWebAuthnByDefault(t *testing.T) { |
| 192 | - | t.Parallel() | 191 | + | commandLine := newMutableCommandLineStub() |
| 192 | + | configureCommandLine(commandLine) | |||
| 193 | + | ||||
| 194 | + | want := "WebAuth" | |||
| 195 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want { | |||
| 196 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |||
| 197 | + | } | |||
| 198 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want { | |||
| 199 | + | t.Fatalf("disable-blink-features = %q, want %q", got, want) | |||
| 200 | + | } | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | func TestConfigureCommandLine_WebAuthnUnsafeOptInDoesNotDisableWebAuth(t *testing.T) { | |||
| 204 | + | t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1") | |||
| 193 | 205 | ||||
| 206 | + | commandLine := newMutableCommandLineStub() | |||
| 207 | + | configureCommandLine(commandLine) | |||
| 208 | + | ||||
| 209 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); strings.Contains(got, "WebAuth") { | |||
| 210 | + | t.Fatalf("disable-features = %q, should not contain WebAuth", got) | |||
| 211 | + | } | |||
| 212 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); strings.Contains(got, "WebAuth") { | |||
| 213 | + | t.Fatalf("disable-blink-features = %q, should not contain WebAuth", got) | |||
| 214 | + | } | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | func TestConfigureCommandLine_WebAuthnDisablePreservesExistingDisableFeatures(t *testing.T) { | |||
| 218 | + | commandLine := newMutableCommandLineStub() | |||
| 219 | + | commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth") | |||
| 220 | + | commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "ExistingBlinkFeature,WebAuth") | |||
| 221 | + | ||||
| 222 | + | configureCommandLine(commandLine) | |||
| 223 | + | ||||
| 224 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,WebAuth" { | |||
| 225 | + | t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,WebAuth") | |||
| 226 | + | } | |||
| 227 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature,WebAuth" { | |||
| 228 | + | t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature,WebAuth") | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | func TestAppendUniqueCommaSeparatedSwitchValues(t *testing.T) { | |||
| 233 | + | t.Run("trims whitespace and skips empty existing values", func(t *testing.T) { | |||
| 234 | + | commandLine := newMutableCommandLineStub() | |||
| 235 | + | commandLine.AppendSwitchWithValue("disable-features", " , ExistingFeature , ") | |||
| 236 | + | ||||
| 237 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA") | |||
| 238 | + | ||||
| 239 | + | want := "ExistingFeature,FeatureA" | |||
| 240 | + | if got := commandLine.GetSwitchValue("disable-features"); got != want { | |||
| 241 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |||
| 242 | + | } | |||
| 243 | + | }) | |||
| 244 | + | ||||
| 245 | + | t.Run("deduplicates existing and appended values", func(t *testing.T) { | |||
| 246 | + | commandLine := newMutableCommandLineStub() | |||
| 247 | + | commandLine.AppendSwitchWithValue("disable-features", "FeatureA,FeatureA") | |||
| 248 | + | ||||
| 249 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA", "FeatureB") | |||
| 250 | + | ||||
| 251 | + | want := "FeatureA,FeatureB" | |||
| 252 | + | if got := commandLine.GetSwitchValue("disable-features"); got != want { | |||
| 253 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |||
| 254 | + | } | |||
| 255 | + | }) | |||
| 256 | + | ||||
| 257 | + | t.Run("empty appended values are no-op", func(t *testing.T) { | |||
| 258 | + | commandLine := newMutableCommandLineStub() | |||
| 259 | + | commandLine.AppendSwitchWithValue("disable-features", "ExistingFeature") | |||
| 260 | + | ||||
| 261 | + | appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features") | |||
| 262 | + | ||||
| 263 | + | if got := commandLine.GetSwitchValue("disable-features"); got != "ExistingFeature" { | |||
| 264 | + | t.Fatalf("disable-features = %q, want ExistingFeature", got) | |||
| 265 | + | } | |||
| 266 | + | }) | |||
| 267 | + | ||||
| 268 | + | t.Run("nil command line is no-op", func(_ *testing.T) { | |||
| 269 | + | appendUniqueCommaSeparatedSwitchValues(nil, "disable-features", "FeatureA") | |||
| 270 | + | }) | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | |||
| 194 | commandLine := newMutableCommandLineStub() | 274 | commandLine := newMutableCommandLineStub() | ||
| 195 | commandLine.AppendSwitchWithValue("type", "renderer") | 275 | commandLine.AppendSwitchWithValue("type", "renderer") | ||
| 196 | 276 | ||||
| @@ -200,3 +280,21 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | |||||
| 200 | t.Fatal("expected no-zygote switch to be appended for child processes") | 280 | t.Fatal("expected no-zygote switch to be appended for child processes") | ||
| 201 | } | 281 | } | ||
| 202 | } | 282 | } | ||
| 283 | + | ||||
| 284 | + | func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) { | |||
| 285 | + | commandLine := newMutableCommandLineStub() | |||
| 286 | + | commandLine.AppendSwitchWithValue("type", "renderer") | |||
| 287 | + | ||||
| 288 | + | (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine) | |||
| 289 | + | ||||
| 290 | + | want := "WebAuth" | |||
| 291 | + | if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want { | |||
| 292 | + | t.Fatalf("disable-features = %q, want %q", got, want) | |||
| 293 | + | } | |||
| 294 | + | if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want { | |||
| 295 | + | t.Fatalf("disable-blink-features = %q, want %q", got, want) | |||
| 296 | + | } | |||
| 297 | + | if !commandLine.HasSwitch("no-zygote") { | |||
| 298 | + | t.Fatal("expected no-zygote switch to be appended") | |||
| 299 | + | } | |||
| 300 | + | } | |||
M internal/infrastructure/cef/env.go +8 -1
| @@ -5,7 +5,10 @@ import ( | |||
| 5 | 5 | "strings" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| 8 | - | const cefExternalBeginFrameEnvVar = "DUMBER_CEF_EXTERNAL_BEGIN_FRAME" | |
| 8 | + | const ( | |
| 9 | + | cefExternalBeginFrameEnvVar = "DUMBER_CEF_EXTERNAL_BEGIN_FRAME" | |
| 10 | + | cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE" | |
| 11 | + | ) | |
| 9 | 12 | ||
| 10 | 13 | // envBoolEnabled returns true when the given environment variable is set | |
| 11 | 14 | // to a truthy value ("1", "true", "yes", "on"). | |
| @@ -21,3 +24,7 @@ func envBoolEnabled(envVar string) bool { | |||
| 21 | 24 | func externalBeginFrameEnabled() bool { | |
| 22 | 25 | return envBoolEnabled(cefExternalBeginFrameEnvVar) | |
| 23 | 26 | } | |
| 27 | + | ||
| 28 | + | func cefWebAuthnUnsafeEnabled() bool { | |
| 29 | + | return envBoolEnabled(cefEnableWebAuthnUnsafeEnvVar) | |
| 30 | + | } | |
| @@ -5,7 +5,10 @@ import ( | |||||
| 5 | "strings" | 5 | "strings" | ||
| 6 | ) | 6 | ) | ||
| 7 | 7 | ||||
| 8 | - | const cefExternalBeginFrameEnvVar = "DUMBER_CEF_EXTERNAL_BEGIN_FRAME" | 8 | + | const ( |
| 9 | + | cefExternalBeginFrameEnvVar = "DUMBER_CEF_EXTERNAL_BEGIN_FRAME" | |||
| 10 | + | cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE" | |||
| 11 | + | ) | |||
| 9 | 12 | ||||
| 10 | // envBoolEnabled returns true when the given environment variable is set | 13 | // envBoolEnabled returns true when the given environment variable is set | ||
| 11 | // to a truthy value ("1", "true", "yes", "on"). | 14 | // to a truthy value ("1", "true", "yes", "on"). | ||
| @@ -21,3 +24,7 @@ func envBoolEnabled(envVar string) bool { | |||||
| 21 | func externalBeginFrameEnabled() bool { | 24 | func externalBeginFrameEnabled() bool { | ||
| 22 | return envBoolEnabled(cefExternalBeginFrameEnvVar) | 25 | return envBoolEnabled(cefExternalBeginFrameEnvVar) | ||
| 23 | } | 26 | } | ||
| 27 | + | ||||
| 28 | + | func cefWebAuthnUnsafeEnabled() bool { | |||
| 29 | + | return envBoolEnabled(cefEnableWebAuthnUnsafeEnvVar) | |||
| 30 | + | } | |||
M internal/infrastructure/cef/env_test.go +31 -0
| @@ -32,3 +32,34 @@ func TestExternalBeginFrameEnabledIsOptIn(t *testing.T) { | |||
| 32 | 32 | }) | |
| 33 | 33 | } | |
| 34 | 34 | } | |
| 35 | + | ||
| 36 | + | func TestCEFWebAuthnUnsafeEnabledIsOptIn(t *testing.T) { | |
| 37 | + | tests := []struct { | |
| 38 | + | name string | |
| 39 | + | value string | |
| 40 | + | want bool | |
| 41 | + | }{ | |
| 42 | + | {name: "1", value: "1", want: true}, | |
| 43 | + | {name: "true", value: "true", want: true}, | |
| 44 | + | {name: "yes", value: "yes", want: true}, | |
| 45 | + | {name: "on", value: "on", want: true}, | |
| 46 | + | {name: "TRUE", value: "TRUE", want: true}, | |
| 47 | + | {name: " true ", value: " true ", want: true}, | |
| 48 | + | {name: "", value: "", want: false}, | |
| 49 | + | {name: "false", value: "false", want: false}, | |
| 50 | + | {name: "0", value: "0", want: false}, | |
| 51 | + | {name: "no", value: "no", want: false}, | |
| 52 | + | {name: "off", value: "off", want: false}, | |
| 53 | + | {name: "random", value: "random", want: false}, | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | for _, tt := range tests { | |
| 57 | + | t.Run(tt.name, func(t *testing.T) { | |
| 58 | + | t.Setenv(cefEnableWebAuthnUnsafeEnvVar, tt.value) | |
| 59 | + | ||
| 60 | + | if got := cefWebAuthnUnsafeEnabled(); got != tt.want { | |
| 61 | + | t.Fatalf("cefWebAuthnUnsafeEnabled(%q) = %v, want %v", tt.value, got, tt.want) | |
| 62 | + | } | |
| 63 | + | }) | |
| 64 | + | } | |
| 65 | + | } | |
| @@ -32,3 +32,34 @@ func TestExternalBeginFrameEnabledIsOptIn(t *testing.T) { | |||||
| 32 | }) | 32 | }) | ||
| 33 | } | 33 | } | ||
| 34 | } | 34 | } | ||
| 35 | + | ||||
| 36 | + | func TestCEFWebAuthnUnsafeEnabledIsOptIn(t *testing.T) { | |||
| 37 | + | tests := []struct { | |||
| 38 | + | name string | |||
| 39 | + | value string | |||
| 40 | + | want bool | |||
| 41 | + | }{ | |||
| 42 | + | {name: "1", value: "1", want: true}, | |||
| 43 | + | {name: "true", value: "true", want: true}, | |||
| 44 | + | {name: "yes", value: "yes", want: true}, | |||
| 45 | + | {name: "on", value: "on", want: true}, | |||
| 46 | + | {name: "TRUE", value: "TRUE", want: true}, | |||
| 47 | + | {name: " true ", value: " true ", want: true}, | |||
| 48 | + | {name: "", value: "", want: false}, | |||
| 49 | + | {name: "false", value: "false", want: false}, | |||
| 50 | + | {name: "0", value: "0", want: false}, | |||
| 51 | + | {name: "no", value: "no", want: false}, | |||
| 52 | + | {name: "off", value: "off", want: false}, | |||
| 53 | + | {name: "random", value: "random", want: false}, | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | for _, tt := range tests { | |||
| 57 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 58 | + | t.Setenv(cefEnableWebAuthnUnsafeEnvVar, tt.value) | |||
| 59 | + | ||||
| 60 | + | if got := cefWebAuthnUnsafeEnabled(); got != tt.want { | |||
| 61 | + | t.Fatalf("cefWebAuthnUnsafeEnabled(%q) = %v, want %v", tt.value, got, tt.want) | |||
| 62 | + | } | |||
| 63 | + | }) | |||
| 64 | + | } | |||
| 65 | + | } | |||