-
Notifications
You must be signed in to change notification settings - Fork 67
browser live view recursion guard #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| package policy | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
| ) | ||
|
|
||
| const ( | ||
| // ChromiumRecursionGuardURLBlocklistEnv configures URLBlocklist entries | ||
| // that are always merged into Chromium policy before the browser starts. | ||
| // Values may be comma- or newline-separated. Unset or blank uses the default. | ||
| // Set to "0", "false", "off", or "none" to disable the guard explicitly. | ||
| ChromiumRecursionGuardURLBlocklistEnv = "CHROMIUM_RECURSION_GUARD_URL_BLOCKLIST" | ||
|
|
||
| // Chrome URLBlocklist matches a bare host and any subdomain; a leading dot | ||
| // would disable subdomain matching. | ||
| // The query is intentionally omitted so livestream JWT variations are | ||
| // covered without per-request proxy work. | ||
| DefaultChromiumRecursionGuardURLBlocklist = "https://onkernel.com:8443/browser/live" | ||
| ) | ||
|
|
||
| // RecursionGuardURLBlocklistFromEnv returns the configured recursion guard | ||
| // URLBlocklist entries. The default blocks deployed live-browser capture URLs. | ||
| func RecursionGuardURLBlocklistFromEnv() []string { | ||
| return recursionGuardURLBlocklistFromLookup(os.LookupEnv) | ||
| } | ||
|
|
||
| func recursionGuardURLBlocklistFromLookup(lookup func(string) (string, bool)) []string { | ||
| value, ok := lookup(ChromiumRecursionGuardURLBlocklistEnv) | ||
| if !ok || strings.TrimSpace(value) == "" { | ||
| return []string{DefaultChromiumRecursionGuardURLBlocklist} | ||
| } | ||
|
|
||
| value = strings.TrimSpace(value) | ||
| switch strings.ToLower(value) { | ||
| case "0", "false", "off", "none": | ||
| return nil | ||
| } | ||
|
|
||
| return splitURLBlocklist(value) | ||
| } | ||
|
|
||
| func splitURLBlocklist(value string) []string { | ||
| fields := strings.FieldsFunc(value, func(r rune) bool { | ||
| return r == ',' || r == '\n' || r == '\r' | ||
| }) | ||
|
|
||
| out := make([]string, 0, len(fields)) | ||
| for _, field := range fields { | ||
| if v := strings.TrimSpace(field); v != "" { | ||
| out = append(out, v) | ||
| } | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| // ApplyURLBlocklistGuard merges guard entries into Chrome's URLBlocklist policy | ||
| // without removing customer-provided blocklist entries. | ||
| func (p *Policy) ApplyURLBlocklistGuard(entries []string) error { | ||
| entries = uniqueStrings(entries) | ||
| if len(entries) == 0 { | ||
| return nil | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disable env leaves blocklistMedium Severity When Reviewed by Cursor Bugbot for commit a8358d5. Configure here. |
||
|
|
||
| return p.Modify(func(current *Policy) error { | ||
| return mergeURLBlocklistGuard(current, entries) | ||
| }) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parallel policy writers lose updatesMedium Severity Each Chromium start now read-modifies-writes Additional Locations (1)Reviewed by Cursor Bugbot for commit a8358d5. Configure here. |
||
|
|
||
| func mergeURLBlocklistGuard(current *Policy, entries []string) error { | ||
| entries = uniqueStrings(entries) | ||
| if len(entries) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| if current.unknownFields == nil { | ||
| current.unknownFields = make(map[string]json.RawMessage) | ||
| } | ||
|
|
||
| var blocklist []string | ||
| if raw, ok := current.unknownFields["URLBlocklist"]; ok && len(raw) > 0 && string(raw) != "null" { | ||
| if err := json.Unmarshal(raw, &blocklist); err != nil { | ||
| return fmt.Errorf("failed to parse existing URLBlocklist policy: %w", err) | ||
| } | ||
| } | ||
|
|
||
| blocklist = append(blocklist, entries...) | ||
| blocklist = uniqueStrings(blocklist) | ||
|
|
||
| raw, err := json.Marshal(blocklist) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to marshal URLBlocklist policy: %w", err) | ||
| } | ||
| current.unknownFields["URLBlocklist"] = raw | ||
| return nil | ||
| } | ||
|
|
||
| func uniqueStrings(values []string) []string { | ||
| seen := make(map[string]struct{}, len(values)) | ||
| out := make([]string, 0, len(values)) | ||
| for _, value := range values { | ||
| value = strings.TrimSpace(value) | ||
| if value == "" { | ||
| continue | ||
| } | ||
| if _, ok := seen[value]; ok { | ||
| continue | ||
| } | ||
| seen[value] = struct{}{} | ||
| out = append(out, value) | ||
| } | ||
| return out | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package policy | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestParseRecursionGuardURLBlocklist_DefaultWhenUnsetOrBlank(t *testing.T) { | ||
| unsetLookup := func(string) (string, bool) { return "", false } | ||
| blankLookup := func(string) (string, bool) { return " ", true } | ||
|
|
||
| assert.Equal(t, []string{DefaultChromiumRecursionGuardURLBlocklist}, recursionGuardURLBlocklistFromLookup(unsetLookup)) | ||
| assert.Equal(t, []string{DefaultChromiumRecursionGuardURLBlocklist}, recursionGuardURLBlocklistFromLookup(blankLookup)) | ||
| } | ||
|
|
||
| func TestParseRecursionGuardURLBlocklist_CustomList(t *testing.T) { | ||
| setLookup := func(string) (string, bool) { | ||
| return "https://example.com:8443/browser/live,\nhttps://capture.example.net/live", true | ||
| } | ||
|
|
||
| got := recursionGuardURLBlocklistFromLookup(setLookup) | ||
|
|
||
| assert.Equal(t, []string{ | ||
| "https://example.com:8443/browser/live", | ||
| "https://capture.example.net/live", | ||
| }, got) | ||
| } | ||
|
|
||
| func TestParseRecursionGuardURLBlocklist_DisableValues(t *testing.T) { | ||
| for _, value := range []string{"0", "false", "off", "none"} { | ||
| t.Run(value, func(t *testing.T) { | ||
| setLookup := func(string) (string, bool) { return value, true } | ||
| assert.Nil(t, recursionGuardURLBlocklistFromLookup(setLookup)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestMergeURLBlocklistGuard_AppendsWithoutDroppingExistingEntries(t *testing.T) { | ||
| current := &Policy{ | ||
| ExtensionSettings: map[string]ExtensionSetting{}, | ||
| unknownFields: map[string]json.RawMessage{ | ||
| "URLBlocklist": json.RawMessage(`["https://blocked.example.com/*"]`), | ||
| }, | ||
| } | ||
|
|
||
| err := mergeURLBlocklistGuard(current, []string{ | ||
| DefaultChromiumRecursionGuardURLBlocklist, | ||
| DefaultChromiumRecursionGuardURLBlocklist, | ||
| }) | ||
| require.NoError(t, err) | ||
|
|
||
| var blocklist []string | ||
| require.NoError(t, json.Unmarshal(current.unknownFields["URLBlocklist"], &blocklist)) | ||
| assert.Equal(t, []string{ | ||
| "https://blocked.example.com/*", | ||
| DefaultChromiumRecursionGuardURLBlocklist, | ||
| }, blocklist) | ||
| } | ||
|
|
||
| func TestMergeURLBlocklistGuard_PreservesOtherUnknownPolicyFields(t *testing.T) { | ||
| current := &Policy{ | ||
| ExtensionSettings: map[string]ExtensionSetting{}, | ||
| unknownFields: map[string]json.RawMessage{ | ||
| "PasswordManagerEnabled": json.RawMessage(`false`), | ||
| }, | ||
| } | ||
|
|
||
| err := mergeURLBlocklistGuard(current, []string{DefaultChromiumRecursionGuardURLBlocklist}) | ||
| require.NoError(t, err) | ||
|
|
||
| assert.JSONEq(t, `false`, string(current.unknownFields["PasswordManagerEnabled"])) | ||
|
|
||
| var blocklist []string | ||
| require.NoError(t, json.Unmarshal(current.unknownFields["URLBlocklist"], &blocklist)) | ||
| assert.Equal(t, []string{DefaultChromiumRecursionGuardURLBlocklist}, blocklist) | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default blocklist port mismatch
Medium Severity
The default guard pattern
https://onkernel.com:8443/browser/liveincludes port8443, while product docs describe live view on port443. ChromeURLBlocklistfilters with an explicit port generally match that port only, so navigations tohttps://onkernel.com/browser/liveon implicit HTTPS443may not be blocked and nested live view can still recurse.Reviewed by Cursor Bugbot for commit a8358d5. Configure here.