detached bda4c7f

Merge pull request #240 from bnema/fix/popup-split-idempotent-attach

Disable WebAuthn in CEF OSR panes
commit
bda4c7fff86ea1d6881139cb984427c5fed51e48
author
bnema · · 2026-04-29 17:42 UTC
committer
GitHub · · 2026-04-29 17:42 UTC
merge of
4613d9c + db4efc4 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
5 files · +302 -6
M docs/reference/permissions.md +8 -0
@@ -23,6 +23,14 @@ These permissions require explicit user approval:
2323
2424 When both microphone and camera are requested together, a combined dialog is shown.
2525
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+
2634 ## Permission Dialog
2735
2836 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 Dialog34 ## 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 +119 -1
@@ -8,7 +8,18 @@ import (
88 "github.com/bnema/dumber/internal/logging"
99 )
1010
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+}
1223
1324 func dumbSchemeOptions() int32 {
1425 return purecef.SchemeOptionsSchemeOptionStandard |
@@ -38,6 +49,99 @@ func configureCommandLine(commandLine purecef.CommandLine) {
3849 // Reddit autoplay muted videos in the feed; without this policy
3950 // Chromium blocks them, showing an infinite spinner.
4051 commandLine.AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required")
52+
53+ configureWebAuthnFeaturePolicy(commandLine)
54+}
55+
56+func configureWebAuthnFeaturePolicy(commandLine purecef.CommandLine) {
57+ if commandLine == nil {
58+ return
59+ }
60+ if cefWebAuthnUnsafeEnabled() {
61+ removeCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
62+ removeCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
63+ return
64+ }
65+ appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
66+ appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
67+}
68+
69+func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
70+ if commandLine == nil || len(values) == 0 {
71+ return
72+ }
73+
74+ seen := make(map[string]struct{}, len(values))
75+ combined := make([]string, 0, len(values))
76+ for _, value := range strings.Split(commandLine.GetSwitchValue(name), ",") {
77+ value = strings.TrimSpace(value)
78+ if value == "" {
79+ continue
80+ }
81+ if _, ok := seen[value]; ok {
82+ continue
83+ }
84+ seen[value] = struct{}{}
85+ combined = append(combined, value)
86+ }
87+ for _, value := range values {
88+ value = strings.TrimSpace(value)
89+ if value == "" {
90+ continue
91+ }
92+ if _, ok := seen[value]; ok {
93+ continue
94+ }
95+ seen[value] = struct{}{}
96+ combined = append(combined, value)
97+ }
98+
99+ if len(combined) == 0 {
100+ return
101+ }
102+ commandLine.AppendSwitchWithValue(name, strings.Join(combined, ","))
103+}
104+
105+func removeCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
106+ if commandLine == nil || len(values) == 0 {
107+ return
108+ }
109+
110+ existing := commandLine.GetSwitchValue(name)
111+ if existing == "" {
112+ return
113+ }
114+
115+ removeSet := make(map[string]struct{}, len(values))
116+ for _, v := range values {
117+ removeSet[strings.TrimSpace(v)] = struct{}{}
118+ }
119+
120+ tokens := strings.Split(existing, ",")
121+ cleaned := make([]string, 0, len(tokens))
122+ removed := false
123+ for _, token := range tokens {
124+ token = strings.TrimSpace(token)
125+ if token == "" {
126+ continue
127+ }
128+ if _, ok := removeSet[token]; ok {
129+ removed = true
130+ continue
131+ }
132+ cleaned = append(cleaned, token)
133+ }
134+
135+ if !removed {
136+ return
137+ }
138+
139+ if len(cleaned) == 0 {
140+ commandLine.RemoveSwitch(name)
141+ return
142+ }
143+
144+ commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ","))
41145 }
42146
43147 // dumberApp implements purecef.App to provide custom scheme registration,
@@ -74,6 +178,19 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin
74178 if commandLine != nil {
75179 configureCommandLine(commandLine)
76180
181+ if processType == "" {
182+ if cefWebAuthnUnsafeEnabled() {
183+ log.Warn().
184+ Str("env_var", cefEnableWebAuthnUnsafeEnvVar).
185+ Msg("cef: WebAuthn enabled via unsupported developer override")
186+ } else {
187+ log.Info().
188+ Strs("disabled_features", cefWebAuthnFeaturesDisabledByPolicy).
189+ Str("override_env_var", cefEnableWebAuthnUnsafeEnvVar).
190+ Msg("cef: WebAuthn disabled for CEF windowless/Alloy runtime")
191+ }
192+ }
193+
77194 cmdline := commandLine.GetCommandLineString()
78195 if len(cmdline) > maxCmdLineLogLen {
79196 runes := []rune(cmdline)
@@ -124,6 +241,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
124241 useAngle := ""
125242 ozonePlatform := ""
126243 if commandLine != nil {
244+ configureWebAuthnFeaturePolicy(commandLine)
127245 appendSwitchIfMissing(commandLine, "no-zygote")
128246 processType = commandLine.GetSwitchValue("type")
129247 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,99 @@ func configureCommandLine(commandLine purecef.CommandLine) {
38 // Reddit autoplay muted videos in the feed; without this policy49 // 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 {
58+ return
59+ }
60+ if cefWebAuthnUnsafeEnabled() {
61+ removeCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
62+ removeCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
63+ return
64+ }
65+ appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
66+ appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
67+}
68+
69+func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
70+ if commandLine == nil || len(values) == 0 {
71+ return
72+ }
73+
74+ seen := make(map[string]struct{}, len(values))
75+ combined := make([]string, 0, len(values))
76+ for _, value := range strings.Split(commandLine.GetSwitchValue(name), ",") {
77+ value = strings.TrimSpace(value)
78+ if value == "" {
79+ continue
80+ }
81+ if _, ok := seen[value]; ok {
82+ continue
83+ }
84+ seen[value] = struct{}{}
85+ combined = append(combined, value)
86+ }
87+ for _, value := range values {
88+ value = strings.TrimSpace(value)
89+ if value == "" {
90+ continue
91+ }
92+ if _, ok := seen[value]; ok {
93+ continue
94+ }
95+ seen[value] = struct{}{}
96+ combined = append(combined, value)
97+ }
98+
99+ if len(combined) == 0 {
100+ return
101+ }
102+ commandLine.AppendSwitchWithValue(name, strings.Join(combined, ","))
103+}
104+
105+func removeCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
106+ if commandLine == nil || len(values) == 0 {
107+ return
108+ }
109+
110+ existing := commandLine.GetSwitchValue(name)
111+ if existing == "" {
112+ return
113+ }
114+
115+ removeSet := make(map[string]struct{}, len(values))
116+ for _, v := range values {
117+ removeSet[strings.TrimSpace(v)] = struct{}{}
118+ }
119+
120+ tokens := strings.Split(existing, ",")
121+ cleaned := make([]string, 0, len(tokens))
122+ removed := false
123+ for _, token := range tokens {
124+ token = strings.TrimSpace(token)
125+ if token == "" {
126+ continue
127+ }
128+ if _, ok := removeSet[token]; ok {
129+ removed = true
130+ continue
131+ }
132+ cleaned = append(cleaned, token)
133+ }
134+
135+ if !removed {
136+ return
137+ }
138+
139+ if len(cleaned) == 0 {
140+ commandLine.RemoveSwitch(name)
141+ return
142+ }
143+
144+ commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ","))
41 }145 }
42 146
43 // dumberApp implements purecef.App to provide custom scheme registration,147 // dumberApp implements purecef.App to provide custom scheme registration,
@@ -74,6 +178,19 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin
74 if commandLine != nil {178 if commandLine != nil {
75 configureCommandLine(commandLine)179 configureCommandLine(commandLine)
76 180
181+ if processType == "" {
182+ if cefWebAuthnUnsafeEnabled() {
183+ log.Warn().
184+ Str("env_var", cefEnableWebAuthnUnsafeEnvVar).
185+ Msg("cef: WebAuthn enabled via unsupported developer override")
186+ } else {
187+ log.Info().
188+ Strs("disabled_features", cefWebAuthnFeaturesDisabledByPolicy).
189+ Str("override_env_var", cefEnableWebAuthnUnsafeEnvVar).
190+ Msg("cef: WebAuthn disabled for CEF windowless/Alloy runtime")
191+ }
192+ }
193+
77 cmdline := commandLine.GetCommandLineString()194 cmdline := commandLine.GetCommandLineString()
78 if len(cmdline) > maxCmdLineLogLen {195 if len(cmdline) > maxCmdLineLogLen {
79 runes := []rune(cmdline)196 runes := []rune(cmdline)
@@ -124,6 +241,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
124 useAngle := ""241 useAngle := ""
125 ozonePlatform := ""242 ozonePlatform := ""
126 if commandLine != nil {243 if commandLine != nil {
244+ configureWebAuthnFeaturePolicy(commandLine)
127 appendSwitchIfMissing(commandLine, "no-zygote")245 appendSwitchIfMissing(commandLine, "no-zygote")
128 processType = commandLine.GetSwitchValue("type")246 processType = commandLine.GetSwitchValue("type")
129 commandLineString = commandLine.GetCommandLineString()247 commandLineString = commandLine.GetCommandLineString()
M internal/infrastructure/cef/app_test.go +136 -4
@@ -2,6 +2,7 @@ package cef
22
33 import (
44 "context"
5+ "strings"
56 "testing"
67 "unsafe"
78
@@ -175,8 +176,6 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testin
175176 }
176177
177178 func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) {
178- t.Parallel()
179-
180179 commandLine := newMutableCommandLineStub()
181180 configureCommandLine(commandLine)
182181
@@ -188,9 +187,122 @@ func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) {
188187 }
189188 }
190189
191-func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
192- t.Parallel()
190+func TestConfigureCommandLine_DisablesWebAuthnByDefault(t *testing.T) {
191+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
192+
193+ commandLine := newMutableCommandLineStub()
194+ configureCommandLine(commandLine)
195+
196+ want := "WebAuth"
197+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want {
198+ t.Fatalf("disable-features = %q, want %q", got, want)
199+ }
200+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want {
201+ t.Fatalf("disable-blink-features = %q, want %q", got, want)
202+ }
203+}
204+
205+func TestConfigureCommandLine_WebAuthnUnsafeOptInDoesNotDisableWebAuth(t *testing.T) {
206+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
207+
208+ commandLine := newMutableCommandLineStub()
209+ configureCommandLine(commandLine)
210+
211+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); strings.Contains(got, "WebAuth") {
212+ t.Fatalf("disable-features = %q, should not contain WebAuth", got)
213+ }
214+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); strings.Contains(got, "WebAuth") {
215+ t.Fatalf("disable-blink-features = %q, should not contain WebAuth", got)
216+ }
217+}
218+
219+func TestConfigureCommandLine_WebAuthnDisablePreservesExistingDisableFeatures(t *testing.T) {
220+ commandLine := newMutableCommandLineStub()
221+ commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth")
222+ commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "ExistingBlinkFeature,WebAuth")
223+
224+ configureCommandLine(commandLine)
225+
226+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,WebAuth" {
227+ t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,WebAuth")
228+ }
229+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature,WebAuth" {
230+ t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature,WebAuth")
231+ }
232+}
233+
234+func TestConfigureCommandLine_WebAuthnUnsafeOverrideRemovesExistingWebAuthTokens(t *testing.T) {
235+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
236+
237+ commandLine := newMutableCommandLineStub()
238+ commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth,AnotherFeature")
239+ commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "WebAuth,ExistingBlinkFeature")
240+
241+ configureCommandLine(commandLine)
242+
243+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,AnotherFeature" {
244+ t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,AnotherFeature")
245+ }
246+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature" {
247+ t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature")
248+ }
249+}
193250
251+func TestConfigureCommandLine_WebAuthnUnsafeOverrideRemovesSwitchWhenOnlyWebAuth(t *testing.T) {
252+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
253+
254+ commandLine := newMutableCommandLineStub()
255+ commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "WebAuth")
256+
257+ configureCommandLine(commandLine)
258+
259+ if commandLine.HasSwitch(chromiumDisableFeaturesSwitch) {
260+ t.Fatalf("disable-features switch should have been removed entirely, but has value %q", commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch))
261+ }
262+}
263+
264+func TestAppendUniqueCommaSeparatedSwitchValues(t *testing.T) {
265+ t.Run("trims whitespace and skips empty existing values", func(t *testing.T) {
266+ commandLine := newMutableCommandLineStub()
267+ commandLine.AppendSwitchWithValue("disable-features", " , ExistingFeature , ")
268+
269+ appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA")
270+
271+ want := "ExistingFeature,FeatureA"
272+ if got := commandLine.GetSwitchValue("disable-features"); got != want {
273+ t.Fatalf("disable-features = %q, want %q", got, want)
274+ }
275+ })
276+
277+ t.Run("deduplicates existing and appended values", func(t *testing.T) {
278+ commandLine := newMutableCommandLineStub()
279+ commandLine.AppendSwitchWithValue("disable-features", "FeatureA,FeatureA")
280+
281+ appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA", "FeatureB")
282+
283+ want := "FeatureA,FeatureB"
284+ if got := commandLine.GetSwitchValue("disable-features"); got != want {
285+ t.Fatalf("disable-features = %q, want %q", got, want)
286+ }
287+ })
288+
289+ t.Run("empty appended values are no-op", func(t *testing.T) {
290+ commandLine := newMutableCommandLineStub()
291+ commandLine.AppendSwitchWithValue("disable-features", "ExistingFeature")
292+
293+ appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features")
294+
295+ if got := commandLine.GetSwitchValue("disable-features"); got != "ExistingFeature" {
296+ t.Fatalf("disable-features = %q, want ExistingFeature", got)
297+ }
298+ })
299+
300+ t.Run("nil command line is no-op", func(_ *testing.T) {
301+ appendUniqueCommaSeparatedSwitchValues(nil, "disable-features", "FeatureA")
302+ })
303+}
304+
305+func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
194306 commandLine := newMutableCommandLineStub()
195307 commandLine.AppendSwitchWithValue("type", "renderer")
196308
@@ -200,3 +312,23 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
200312 t.Fatal("expected no-zygote switch to be appended for child processes")
201313 }
202314 }
315+
316+func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) {
317+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
318+
319+ commandLine := newMutableCommandLineStub()
320+ commandLine.AppendSwitchWithValue("type", "renderer")
321+
322+ (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine)
323+
324+ want := "WebAuth"
325+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want {
326+ t.Fatalf("disable-features = %q, want %q", got, want)
327+ }
328+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want {
329+ t.Fatalf("disable-blink-features = %q, want %q", got, want)
330+ }
331+ if !commandLine.HasSwitch("no-zygote") {
332+ t.Fatal("expected no-zygote switch to be appended")
333+ }
334+}
@@ -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,122 @@ 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+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
192+
193+ commandLine := newMutableCommandLineStub()
194+ configureCommandLine(commandLine)
195+
196+ want := "WebAuth"
197+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want {
198+ t.Fatalf("disable-features = %q, want %q", got, want)
199+ }
200+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want {
201+ t.Fatalf("disable-blink-features = %q, want %q", got, want)
202+ }
203+}
204+
205+func TestConfigureCommandLine_WebAuthnUnsafeOptInDoesNotDisableWebAuth(t *testing.T) {
206+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
207+
208+ commandLine := newMutableCommandLineStub()
209+ configureCommandLine(commandLine)
210+
211+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); strings.Contains(got, "WebAuth") {
212+ t.Fatalf("disable-features = %q, should not contain WebAuth", got)
213+ }
214+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); strings.Contains(got, "WebAuth") {
215+ t.Fatalf("disable-blink-features = %q, should not contain WebAuth", got)
216+ }
217+}
218+
219+func TestConfigureCommandLine_WebAuthnDisablePreservesExistingDisableFeatures(t *testing.T) {
220+ commandLine := newMutableCommandLineStub()
221+ commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth")
222+ commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "ExistingBlinkFeature,WebAuth")
223+
224+ configureCommandLine(commandLine)
225+
226+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,WebAuth" {
227+ t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,WebAuth")
228+ }
229+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature,WebAuth" {
230+ t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature,WebAuth")
231+ }
232+}
233+
234+func TestConfigureCommandLine_WebAuthnUnsafeOverrideRemovesExistingWebAuthTokens(t *testing.T) {
235+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
236+
237+ commandLine := newMutableCommandLineStub()
238+ commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth,AnotherFeature")
239+ commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "WebAuth,ExistingBlinkFeature")
240+
241+ configureCommandLine(commandLine)
242+
243+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,AnotherFeature" {
244+ t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,AnotherFeature")
245+ }
246+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature" {
247+ t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature")
248+ }
249+}
193 250
251+func TestConfigureCommandLine_WebAuthnUnsafeOverrideRemovesSwitchWhenOnlyWebAuth(t *testing.T) {
252+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
253+
254+ commandLine := newMutableCommandLineStub()
255+ commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "WebAuth")
256+
257+ configureCommandLine(commandLine)
258+
259+ if commandLine.HasSwitch(chromiumDisableFeaturesSwitch) {
260+ t.Fatalf("disable-features switch should have been removed entirely, but has value %q", commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch))
261+ }
262+}
263+
264+func TestAppendUniqueCommaSeparatedSwitchValues(t *testing.T) {
265+ t.Run("trims whitespace and skips empty existing values", func(t *testing.T) {
266+ commandLine := newMutableCommandLineStub()
267+ commandLine.AppendSwitchWithValue("disable-features", " , ExistingFeature , ")
268+
269+ appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA")
270+
271+ want := "ExistingFeature,FeatureA"
272+ if got := commandLine.GetSwitchValue("disable-features"); got != want {
273+ t.Fatalf("disable-features = %q, want %q", got, want)
274+ }
275+ })
276+
277+ t.Run("deduplicates existing and appended values", func(t *testing.T) {
278+ commandLine := newMutableCommandLineStub()
279+ commandLine.AppendSwitchWithValue("disable-features", "FeatureA,FeatureA")
280+
281+ appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA", "FeatureB")
282+
283+ want := "FeatureA,FeatureB"
284+ if got := commandLine.GetSwitchValue("disable-features"); got != want {
285+ t.Fatalf("disable-features = %q, want %q", got, want)
286+ }
287+ })
288+
289+ t.Run("empty appended values are no-op", func(t *testing.T) {
290+ commandLine := newMutableCommandLineStub()
291+ commandLine.AppendSwitchWithValue("disable-features", "ExistingFeature")
292+
293+ appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features")
294+
295+ if got := commandLine.GetSwitchValue("disable-features"); got != "ExistingFeature" {
296+ t.Fatalf("disable-features = %q, want ExistingFeature", got)
297+ }
298+ })
299+
300+ t.Run("nil command line is no-op", func(_ *testing.T) {
301+ appendUniqueCommaSeparatedSwitchValues(nil, "disable-features", "FeatureA")
302+ })
303+}
304+
305+func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
194 commandLine := newMutableCommandLineStub()306 commandLine := newMutableCommandLineStub()
195 commandLine.AppendSwitchWithValue("type", "renderer")307 commandLine.AppendSwitchWithValue("type", "renderer")
196 308
@@ -200,3 +312,23 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
200 t.Fatal("expected no-zygote switch to be appended for child processes")312 t.Fatal("expected no-zygote switch to be appended for child processes")
201 }313 }
202 }314 }
315+
316+func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) {
317+ t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
318+
319+ commandLine := newMutableCommandLineStub()
320+ commandLine.AppendSwitchWithValue("type", "renderer")
321+
322+ (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine)
323+
324+ want := "WebAuth"
325+ if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want {
326+ t.Fatalf("disable-features = %q, want %q", got, want)
327+ }
328+ if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want {
329+ t.Fatalf("disable-blink-features = %q, want %q", got, want)
330+ }
331+ if !commandLine.HasSwitch("no-zygote") {
332+ t.Fatal("expected no-zygote switch to be appended")
333+ }
334+}
M internal/infrastructure/cef/env.go +8 -1
@@ -5,7 +5,10 @@ import (
55 "strings"
66 )
77
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+)
912
1013 // envBoolEnabled returns true when the given environment variable is set
1114 // to a truthy value ("1", "true", "yes", "on").
@@ -21,3 +24,7 @@ func envBoolEnabled(envVar string) bool {
2124 func externalBeginFrameEnabled() bool {
2225 return envBoolEnabled(cefExternalBeginFrameEnvVar)
2326 }
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 set13 // 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) {
3232 })
3333 }
3434 }
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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close