11 KiB
WebKit Website Data Isolation Per Nostr Identity
Status: IMPLEMENTED (Phase 0 + Phase A, verified end-to-end).
Problem
When a user logs in, visits a page, logs out, then logs in as a different identity and revisits the same page, the second user sees the first user's data (session cookies, localStorage, cached responses, service workers).
Root Cause
The browser uses the default WebKitWebContext and its
attached WebKitWebsiteDataManager for the entire process
lifetime. That single data manager holds, process-wide:
- HTTP disk + memory cache
- Cookies (the primary leak — session cookies identify you to web pages)
- localStorage / sessionStorage / IndexedDB / WebSQL
- Service worker registrations + offline app cache
- Favicon database (
webkit_web_context_set_favicon_database_directory(web_ctx, NULL))
The logout / identity-switch paths only clear the Nostr signer and do nothing to WebKit's data stores or to the already-loaded tabs:
app_menu_logout_proxy— freesg_state.signer, blanks pubkey, returns.agent_logout—app_clear_signer()+nostr_bridge_set_signer(NULL, ...).agent_switch_identity— frees old signer, logs in new, no tab reload.
Two compounding problems:
- Persistent WebKit data survives the identity switch.
- Live tabs keep user A's DOM/localStorage in memory; wiping the data manager alone does not clear already-loaded pages.
The existing plans/per-user-profiles.md isolates
the SQLite layer (bookmarks, history, session, Nostr events) but does not
cover WebKit's own storage — this plan closes that gap.
Solution (chosen: per-user data manager + clear-on-switch safety net)
Part A — Per-user WebKitWebsiteDataManager
Give each pubkey its own data manager rooted at
~/.sovereign_browser/profiles/<pubkey_hex>/webkit/ so cookies, cache, storage,
service workers, and favicons persist per-user and never cross-contaminate.
WebKitGTK constraint: a WebKitWebContext is bound to one
WebKitWebsiteDataManager for its lifetime — you cannot swap the data manager
on an existing context. Two viable strategies:
Strategy 1 (preferred): per-user WebKitWebContext.
Create a fresh webkit_web_context_new_with_website_data_manager(dm) for each
login. All tabs/webviews are created from this context. On logout/switch,
destroy the old context (which tears down its webviews) and build a new one.
This is the cleanest isolation and matches how Epiphary/GNOME Web does
per-profile isolation.
Strategy 2 (fallback): single context, manual clear + per-user dirs.
Keep the default context but on each login call
webkit_website_data_manager_clear(dm, WEBKIT_WEBSITE_DATA_ALL, 0, NULL, NULL, NULL)
then point the cookie/cache/storage dirs at the per-user path. This is fragile
because the context caches some state internally and the favicon DB directory
is set once at context init.
Recommend Strategy 1. It requires reworking how the context is created and
how tab_manager / nostr_bridge / tor_scheme / nostr_scheme / net_services
reference it (they currently call webkit_web_context_get_default() or receive
the context at init), but it gives true isolation.
Part B — Clear-on-switch safety net
Even with per-user data managers, the live tabs (and the agent chat sidebar webview) hold user A's DOM/localStorage in memory. On any identity change:
- Close all tabs:
tab_manager_close_all()(tab_manager.c:3727). - Destroy the agent chat sidebar webview if present.
- (Strategy 1) Destroy the old
WebKitWebContext+ data manager. - Build the new per-user context + data manager.
- Re-register URI schemes (
sovereign://,nostr://,tor://) on the new context — they are per-context. - Re-init
nostr_bridge,nostr_scheme,tor_scheme,net_servicesagainst the new context. - Re-init
tab_manageragainst the new context (or expose atab_manager_set_context()). - Restore the new user's session (
session_restore()) or open the default new-tab URL.
Architecture
flowchart TD
A[Login complete - pubkey known] --> B[profile_ensure_dir pubkey]
B --> C[Build per-user WebKitWebsiteDataManager<br/>rooted at profiles/pubkey/webkit]
C --> D[Build per-user WebKitWebContext<br/>new_with_website_data_manager]
D --> E[Register sovereign/nostr/tor schemes on new ctx]
E --> F[Re-init nostr_bridge, net_services, tab_manager on new ctx]
F --> G[session_restore or open new-tab URL]
H[Logout / switch_identity] --> I[tab_manager_close_all]
I --> J[Destroy sidebar webview]
J --> K[Destroy old WebKitWebContext + data manager]
K --> L{switch or logout?}
L -->|switch| A
L -->|logout| M[Show login dialog / wait for agent login]
M --> A
Implementation Steps
1. Profile helpers for WebKit data dir
In src/profile.c / src/profile.h add:
profile_get_webkit_dir(const char *pubkey_hex, char *out, size_t out_sz)→~/.sovereign_browser/profiles/<pubkey>/webkit/profile_ensure_webkit_dir(const char *pubkey_hex)— mkdir -p.
2. New module src/web_context.c / src/web_context.h
Centralize context + data manager construction so login/logout/switch share one code path. Functions:
web_context_build_for_pubkey(const char *pubkey_hex)→ returns a newWebKitWebContext*with a per-userWebKitWebsiteDataManager(cookies, cache, local_storage, indexed_db, websql, offline_app_cache, service_worker registrations, itp, hsts all pointed at the per-user webkit dir), favicon DB enabled, TLS errors ignored, security manager configured (sovereign/tor/file secure+local).web_context_teardown(WebKitWebContext *ctx)— unrefs context + data manager, frees per-user state.- Holds the current context pointer (
g_web_ctx) so the rest of the codebase can fetch it withoutwebkit_web_context_get_default().
3. Refactor scheme/bridge/service init to be re-runnable
Currently these are one-shot inits tied to the default context. Make them
accept a WebKitWebContext* and be safe to call again on a new context:
nostr_bridge_register— already takesctx; ensure it can be called twice (track prior registration, disconnect old handlers).nostr_scheme_register— make it takectx(currently uses default) and idempotent.tor_scheme_register— same.net_services_init— currently callswebkit_web_context_get_default()internally (net_services.c:69); passctxin instead.
4. Refactor tab_manager to support context swap
tab_manager_init currently stores the context once.
Add:
tab_manager_set_context(WebKitWebContext *ctx)— updates the stored context so subsequenttab_manager_new_tabcalls use the new context. Existing tabs are already closed (Part B step 1) before this is called.- Or simpler: tear down and re-init
tab_manageron each switch. Pick whichever is less invasive given the static globals intab_manager.c.
5. Wire the login/logout/switch flows
app_menu_logout_proxy: after clearing the signer, run the teardown sequence (close all tabs, destroy sidebar, destroy context) then re-show the login dialog. On successful login, run the build sequence.agent_logout: same teardown, then leave the browser in a logged-out state (no context, or a minimal ephemeral context showing a "logged out" page).agent_switch_identity: teardown old, build new with the new pubkey, restore session.- Initial startup in
main.c: replacewebkit_web_context_get_default()withweb_context_build_for_pubkey()after login completes (this means deferring context creation until after the login dialog — reordering the startup sequence).
6. Favicon database per user
webkit_web_context_set_favicon_database_directory(web_ctx, NULL)
currently uses the default shared location. In web_context_build_for_pubkey,
pass the per-user webkit dir (or a favicons/ subdir) so favicons don't leak
between users.
7. Read-only / no-login mode
When running with --no-login or read-only, there is no pubkey to key the
data dir on. Use an ephemeral data manager
(webkit_website_data_manager_new_ephemeral()) so no data persists at all,
or a shared ~/.sovereign_browser/webkit-anon/ dir. Decide and document.
8. Migration
Existing users have data in WebKit's default location
(~/.cache/sovereign_browser/ or similar). On first login with the new
system, optionally copy the default WebKit data dir into
profiles/<pubkey>/webkit/ so they keep their cookies/cache. Low priority —
can ship without migration and just start fresh per user.
9. Tests
- Manual: log in as user A, visit a site that sets a cookie/localStorage, log out, log in as user B, visit the same site, confirm user A's data is gone and user B gets a fresh session.
- Add a test page under
tests/local-site/that writes a visible marker to localStorage + a cookie, so this is reproducible. - Verify
sovereign://,nostr://,tor://schemes still work after a switch (they are re-registered on the new context). - Verify the agent chat sidebar works after a switch (its webview is rebuilt on the new context).
Files to Modify
- New:
src/web_context.c,src/web_context.h src/profile.c/src/profile.h— webkit dir helperssrc/main.c— startup ordering, logout proxy, useweb_context_*src/agent_login.c—agent_logout,agent_switch_identitysrc/tab_manager.c/src/tab_manager.h— context swap support, sidebar teardownsrc/nostr_bridge.c— re-runnable registrationsrc/nostr_scheme.c— accept ctx, idempotentsrc/tor_scheme.c— accept ctx, idempotentsrc/net_services.c— accept ctx instead of defaultMakefile— addweb_context.o- New test page:
tests/local-site/identity-leak-test.html
Risk Assessment
- High risk — changes how the
WebKitWebContextis created and tears down, which touches every subsystem that references the context. - Startup reordering — context creation must move to after login, which
changes the long-standing flow in
main.c. - Re-runnable scheme registration — WebKit may not support unregistering scheme handlers cleanly; verify that building a fresh context and re-registering works without leaking the old context.
- Sidebar webview — the agent chat sidebar (
tab_manager_toggle_sidebar) keeps a long-lived webview; must be destroyed on context swap or it will hold a ref to the dead context. - Mitigation: implement Part B (clear-on-switch) first as a standalone change — it alone fixes the leak even without per-user dirs. Ship that, then layer Part A (per-user data managers) on top.