How camofox-browser Session Isolation Separates Cookies and Storage Between Users
camofox-browser enforces strict per-user isolation by creating a separate Playwright browser context for every userId, ensuring that cookies, localStorage, and cache from one user are never accessible to another.
The jo-inc/camofox-browser project implements robust multi-user browsing automation by leveraging Playwright's BrowserContext API. Each user receives a completely isolated browser environment that maintains its own cookie jar, storage mechanisms, and cache, eliminating the risk of cross-contamination between sessions.
Architecture Overview
The Session Map
At the core of camofox-browser's isolation strategy lies a centralized session registry implemented as a JavaScript Map. Located in server.js at line 250, this structure maintains the association between users and their private browser environments:
// server.js – line 250
// userId -> { context, tabGroups: Map<sessionKey, Map<tabId, TabState>>, lastAccess }
const sessions = new Map();
The map uses a normalized userId as the key, ensuring consistent lookups regardless of input formatting variations.
Playwright BrowserContext Isolation
The architecture relies on Playwright's native BrowserContext abstraction, which provides complete state separation at the browser level. Each context maintains independent:
- Cookie jars (HTTP and JavaScript cookies)
localStorageandsessionStoragenamespaces- Browser cache and IndexedDB storage
- Proxy configurations and geolocation settings
Because Playwright enforces these boundaries at the browser process level, no JavaScript execution or network request can breach the isolation between contexts.
Implementation Details
Context Creation via getSession()
The getSession(userId) function, spanning lines 701-756 in server.js, serves as the gateway to per-user isolation. When a request targets a specific user, the function either retrieves an existing context or initializes a fresh one:
// server.js – lines 701-756
async function getSession(userId) {
const key = normalizeUserId(userId);
let session = sessions.get(key);
// ... validation logic ...
if (!session) {
const b = await ensureBrowser();
const contextOptions = { /* proxy, locale settings */ };
const context = await b.newContext(contextOptions);
session = {
context,
tabGroups: new Map(),
lastAccess: Date.now(),
proxySessionId: sessionProxy?.sessionId || null
};
sessions.set(key, session);
log('info', 'session created', { userId: key });
}
session.lastAccess = Date.now();
return session;
}
This implementation guarantees that each userId maps to exactly one BrowserContext instance, with optional per-user proxy settings applied during creation.
Cookie Import Endpoint
The POST /sessions/:userId/cookies endpoint (lines 166-242 in server.js) validates and imports cookies exclusively into the requesting user's context. The server sanitizes input objects before injecting them via the context-specific addCookies method:
// server.js – lines 166-242
app.post('/sessions/:userId/cookies', express.json({ limit: '512kb' }), async (req, res) => {
// ... validation and sanitization ...
const session = await getSession(userId);
await session.context.addCookies(sanitized); // ← isolated per-user context
// ... response handling ...
});
Because addCookies operates on a specific BrowserContext instance, imported data remains confined to that user's session jar.
Automatic Cleanup and Context Recreation
The system implements robust error handling to maintain isolation integrity even during browser crashes. Lines 705-712 in server.js contain dead-context detection logic that identifies failed contexts before they can corrupt user data:
// server.js – lines 705-712 (dead-context detection)
try {
session.context.pages(); // throws if context is dead
} catch (err) {
log('warn', 'session context dead, recreating', { userId: key, error: err.message });
session.context.close().catch(() => {});
sessions.delete(key);
}
When a context failure is detected, the system immediately removes the stale entry from the session map. Subsequent requests for that user trigger getSession to spawn a fresh, empty context, ensuring no residual data persists from the failed session.
Verification Through Code Examples
Importing Cookies for a Specific User
The following API request demonstrates how cookies are injected into an isolated user session:
curl -X POST http://localhost:9377/sessions/alice/cookies \
-H "Content-Type: application/json" \
-d '{
"cookies": [
{"name":"sessionid","value":"abc123","domain":"example.com","path":"/"},
{"name":"pref","value":"dark","domain":"example.com"}
]
}'
The server routes this request through getSession("alice"), ensuring the cookies are added only to Alice's context via session.context.addCookies().
Verifying Cross-User Isolation
This pseudo-code from the test suite confirms that user data remains segregated:
// client.js – test suite helper
await client.post('/sessions/alice/cookies', {
cookies: [{ name: 'a', value: '1', domain: 'ex.com' }]
});
await client.post('/sessions/bob/cookies', {
cookies: [{ name: 'b', value: '2', domain: 'ex.com' }]
});
const aliceCookies = await client.get('/sessions/alice/cookies'); // → [{name:'a',…}]
const bobCookies = await client.get('/sessions/bob/cookies'); // → [{name:'b',…}]
Despite targeting the same domain (ex.com), Alice's context contains only cookie a, while Bob's contains only cookie b, proving complete isolation between the BrowserContext instances.
Automatic Context Recreation
When a context becomes unresponsive, the system transparently recreates it without user intervention:
// Simulate context destruction
await client.delete('/sessions/alice'); // destroys Alice's context
await client.post('/sessions/alice/cookies', { // triggers getSession
cookies: [{ name: 'new', value: 'data', domain: 'test.com' }]
});
The new context starts with an empty cookie store and fresh storage backends, maintaining the isolation guarantee.
Summary
- Per-User BrowserContext: camofox-browser creates a separate Playwright
BrowserContextfor everyuserId, providing hardware-level isolation of cookies, storage, and cache. - Centralized Session Map: The
sessionsMap inserver.js(line 250) tracks each user's context and metadata, keyed by normalized user identifiers. - Strict API Boundaries: The
getSession()function and cookie import endpoint ensure all browser state operations target exactly one user's context. - Self-Healing Isolation: Dead context detection automatically removes failed sessions and creates fresh, empty contexts to prevent data leakage or corruption.
- No Cross-Contamination: Playwright's native architecture ensures that even under error conditions, no code path exists that could share a context between different
userIdvalues.
Frequently Asked Questions
How does camofox-browser prevent cookie leakage between users?
camofox-browser prevents cookie leakage by instantiating a separate Playwright BrowserContext for each userId via the getSession() function in server.js. Each context maintains its own independent cookie jar, localStorage, and cache at the browser process level. When the POST /sessions/:userId/cookies endpoint receives data, it calls session.context.addCookies(), which injects cookies only into that specific user's isolated storage.
What happens when a browser context crashes unexpectedly?
When a browser context crashes, the getSession() function detects the failure by invoking session.context.pages() (lines 705-712), which throws an error if the underlying context is dead. The system immediately closes the corrupted context, deletes the entry from the global sessions Map, and creates a fresh BrowserContext with empty storage. This ensures that subsequent requests for that user operate in a clean, isolated environment without residual data from the failed session.
How can existing cookies be imported into a specific user's session?
Clients import cookies by sending a POST request to /sessions/:userId/cookies with a JSON payload containing cookie objects. The server validates and sanitizes these objects before calling session.context.addCookies(sanitized) (lines 166-242 in server.js). Because this operation targets a specific user context retrieved via getSession(userId), the cookies are stored exclusively in that user's browser state and remain inaccessible to other users.
Where is session state stored within the CamoFox server architecture?
Session state is stored in an in-memory Map declared at line 250 of server.js, keyed by normalized userId strings. Each Map entry contains the Playwright BrowserContext instance, a nested Map of tab groups, a timestamp of last access, and optional proxy session identifiers. This design keeps all user-specific browser state—including cookies, HTML5 storage, and cache—encapsulated within the Playwright context object referenced by the Map entry.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →