Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions server/cmd/chromium-launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/kernel/kernel-images/server/lib/chromiumflags"
"github.com/kernel/kernel-images/server/lib/policy"
"github.com/kernel/kernel-images/server/lib/x11"
)

Expand Down Expand Up @@ -52,6 +53,11 @@ func main() {
internalPort = "9223"
}

if err := (&policy.Policy{}).ApplyURLBlocklistGuard(policy.RecursionGuardURLBlocklistFromEnv()); err != nil {
fmt.Fprintf(os.Stderr, "failed applying chromium recursion guard policy: %v\n", err)
os.Exit(1)
}

// Wait for devtools port to be available (handles SIGKILL socket cleanup delay)
waitForPort(internalPort, 5*time.Second)

Expand Down
115 changes: 115 additions & 0 deletions server/lib/policy/recursion_guard.go
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"

Copy link
Copy Markdown

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/live includes port 8443, while product docs describe live view on port 443. Chrome URLBlocklist filters with an explicit port generally match that port only, so navigations to https://onkernel.com/browser/live on implicit HTTPS 443 may not be blocked and nested live view can still recurse.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8358d5. Configure here.

)

// 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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disable env leaves blocklist

Medium Severity

When CHROMIUM_RECURSION_GUARD_URL_BLOCKLIST is set to 0, false, off, or none, parsing returns no entries and ApplyURLBlocklistGuard exits without writing policy. Guard URLs merged on earlier Chromium starts stay in URLBlocklist, so the recursion guard keeps blocking even though the env documents an explicit disable.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8358d5. Configure here.


return p.Modify(func(current *Policy) error {
return mergeURLBlocklistGuard(current, entries)
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parallel policy writers lose updates

Medium Severity

Each Chromium start now read-modifies-writes policy.json via ApplyURLBlocklistGuard, while the API process uses the same Policy.Modify helper. The mutex is in-process only, so concurrent launcher and API updates can overwrite each other and drop extension or policy changes written in between.

Additional Locations (1)
Fix in Cursor Fix in Web

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
}
79 changes: 79 additions & 0 deletions server/lib/policy/recursion_guard_test.go
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)
}
Loading