Skip to content
Closed
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
61 changes: 57 additions & 4 deletions server/cmd/api/api/chromium_configure.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -315,10 +316,8 @@ func (s *ApiService) chromiumConfigureRestart(ctx context.Context, st *chromiumC
if err != nil {
return cfg500ConfigureStep(chromiumConfigureStepProfile, err.Error())
}
if spec.needsNav {
if err := stripProfileSessionRestore(preparedProfile); err != nil {
return cfg500ConfigureStep(chromiumConfigureStepProfile, err.Error())
}
if err := sanitizePreparedProfile(ctx, preparedProfile, spec.needsNav); err != nil {
return cfg500ConfigureStep(chromiumConfigureStepProfile, err.Error())
}
if err := chromiumInstallPreparedProfile(preparedProfile); err != nil {
return cfg500ConfigureStep(chromiumConfigureStepProfile, err.Error())
Expand Down Expand Up @@ -708,6 +707,22 @@ func chromiumPrepareProfileArchive(profilePath string, strip int) (preparedDir s
return preparedDir, cleanup, nil
}

// sanitizePreparedProfile edits the unpacked profile before it replaces
// user-data. Session restore is stripped only when a start_url will be
// navigated; Kernel settings are always stripped from Local State, and a
// failure there is logged rather than failing the load.
func sanitizePreparedProfile(ctx context.Context, preparedDir string, stripSessionRestore bool) error {
if stripSessionRestore {
if err := stripProfileSessionRestore(preparedDir); err != nil {
return err
}
}
if err := stripProfileKernelLocalState(preparedDir); err != nil {
logger.FromContext(ctx).Warn("failed to strip Kernel settings from profile Local State", "error", err)
}
return nil
}

// stripProfileSessionRestore deletes the prepared profile's Default/Sessions so
// Chrome cannot restore its saved tabs after the restart and race the start_url
// navigation. Only the live copy is touched; the stored archive is unchanged.
Expand All @@ -718,6 +733,44 @@ func stripProfileSessionRestore(preparedDir string) error {
return nil
}

// stripProfileKernelLocalState removes the top-level "kernel" object from the
// prepared profile's Local State. Kernel's Chromium build stores browser-wide
// settings set over CDP there (fingerprint config, request header rules), and
// they belong to the browser that set them, not to the profile. Only the live
// copy is touched; the stored archive is unchanged.
func stripProfileKernelLocalState(preparedDir string) error {
path := filepath.Join(preparedDir, "Local State")
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("read Local State: %w", err)
}

dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var state map[string]any
if err := dec.Decode(&state); err != nil {
return fmt.Errorf("parse Local State: %w", err)
}
if _, ok := state["kernel"]; !ok {
return nil
}
delete(state, "kernel")

var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(state); err != nil {
return fmt.Errorf("marshal Local State: %w", err)
}
if err := os.WriteFile(path, bytes.TrimRight(buf.Bytes(), "\n"), 0o644); err != nil {
return fmt.Errorf("write Local State: %w", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Failed rewrite can corrupt Local State

Medium Severity

A write error in stripProfileKernelLocalState still installs the profile. os.WriteFile truncates Local State before writing, so a failed write can leave empty or partial JSON. Configure then swaps that copy in, which can drop os_crypt and other browser-wide prefs instead of keeping the original file.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fd35e98. Configure here.

return nil
}

func chromiumInstallPreparedProfile(preparedDir string) error {
if preparedDir == "" {
return nil
Expand Down
71 changes: 71 additions & 0 deletions server/cmd/api/api/chromium_configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"bytes"
"context"
"errors"
"io"
"mime/multipart"
Expand Down Expand Up @@ -162,6 +163,76 @@ func TestStripProfileSessionRestore(t *testing.T) {
require.NoError(t, stripProfileSessionRestore(prepared))
}

func TestStripProfileKernelLocalState(t *testing.T) {
prepared := t.TempDir()
path := filepath.Join(prepared, "Local State")
input := `{"kernel":{"fingerprint_config":{"version":1},"request_header_rules":{"generation":2}},"browser":{"last_redirect_origin":"https://a.test/?x=1&y=2"},"variations_seed_date":13350000000000000001}`
require.NoError(t, os.WriteFile(path, []byte(input), 0o600))

require.NoError(t, stripProfileKernelLocalState(prepared))

got, err := os.ReadFile(path)
require.NoError(t, err)
require.JSONEq(t, `{"browser":{"last_redirect_origin":"https://a.test/?x=1&y=2"},"variations_seed_date":13350000000000000001}`, string(got))
require.Contains(t, string(got), "13350000000000000001")
info, err := os.Stat(path)
require.NoError(t, err)
require.Equal(t, os.FileMode(0o600), info.Mode().Perm(), "rewrite keeps the existing file mode")

// Without Kernel settings the file is left byte-for-byte unchanged.
untouched := "{\"browser\": {}}\n"
require.NoError(t, os.WriteFile(path, []byte(untouched), 0o600))
require.NoError(t, stripProfileKernelLocalState(prepared))
got, err = os.ReadFile(path)
require.NoError(t, err)
require.Equal(t, untouched, string(got))

// A profile without Local State is a no-op, not an error.
require.NoError(t, os.Remove(path))
require.NoError(t, stripProfileKernelLocalState(prepared))

require.NoError(t, os.WriteFile(path, []byte("{not json"), 0o600))
require.ErrorContains(t, stripProfileKernelLocalState(prepared), "parse Local State")
}

func TestSanitizePreparedProfile(t *testing.T) {
newPrepared := func(t *testing.T, localState string) string {
prepared := t.TempDir()
sessions := filepath.Join(prepared, "Default", "Sessions")
require.NoError(t, os.MkdirAll(sessions, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(sessions, "Session_123"), []byte("tabs"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(prepared, "Local State"), []byte(localState), 0o644))
return prepared
}
const withKernel = `{"kernel":{"fingerprint_config":{"version":1}},"browser":{}}`

t.Run("strips Kernel settings and session restore before navigation", func(t *testing.T) {
prepared := newPrepared(t, withKernel)
require.NoError(t, sanitizePreparedProfile(context.Background(), prepared, true))

require.NoDirExists(t, filepath.Join(prepared, "Default", "Sessions"))
got, err := os.ReadFile(filepath.Join(prepared, "Local State"))
require.NoError(t, err)
require.JSONEq(t, `{"browser":{}}`, string(got))
})

t.Run("strips Kernel settings and keeps session restore without navigation", func(t *testing.T) {
prepared := newPrepared(t, withKernel)
require.NoError(t, sanitizePreparedProfile(context.Background(), prepared, false))

require.DirExists(t, filepath.Join(prepared, "Default", "Sessions"))
got, err := os.ReadFile(filepath.Join(prepared, "Local State"))
require.NoError(t, err)
require.JSONEq(t, `{"browser":{}}`, string(got))
})

t.Run("unreadable Local State does not fail the load", func(t *testing.T) {
prepared := newPrepared(t, "{not json")
require.NoError(t, sanitizePreparedProfile(context.Background(), prepared, true))
require.NoDirExists(t, filepath.Join(prepared, "Default", "Sessions"))
})
}

func TestChromiumValidateFlags(t *testing.T) {
valid := `{"flags":["--kiosk"]}`
plan, err := chromiumValidateFlags(&valid)
Expand Down
Loading