Skip to content

About

Encrypted browser session transfer: MV3 extension + ciphertext-only relay + Playwright MCP applier. Move HttpOnly cookies and localStorage to a remote browser without a third party seeing the tokens.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Session Bridge

Move a logged-in browser session from your desktop browser into a remote browser, encrypted, without a third party ever seeing the tokens.

CI License: MIT Manifest V3

You are logged in to Facebook, Instagram, TikTok on your laptop. You want the same sessions inside a CloakBrowser / Playwright instance on a server, without logging in again and without a service holding your cookies.

Session Bridge captures the session in the browser (chrome.cookies + on-demand page injection), encrypts it with a key derived from a one-time transfer code, ships only ciphertext through a relay you control, and applies it on the other side through Playwright MCP.

      source browser                          your relay                    remote browser
┌────────────────────────┐            ┌────────────────────┐          ┌────────────────────────┐
│  Session Bridge ext.   │ ciphertext │  relay.py          │ciphertext│  apply.py              │
│  • chrome.cookies      │ ─────────▶ │  • bearer auth     │────────▶ │  • decrypt locally     │
│  • scripting injection │            │  • ciphertext only │          │  • storageState        │
│  • AES-256-GCM         │            │  • TTL, one-shot   │          │  • MCP apply + verify  │
└────────────────────────┘            └────────────────────┘          └────────────────────────┘
              │                                                                  ▲
              └──────────────── transfer code, separate channel ─────────────────┘

Why this exists

Every "copy your cookies to my server" tool asks you to trust the server with plaintext session tokens. Session Bridge splits that trust in three:

Component Sees plaintext? Where it runs Trust needed
Extension Yes, it is your browser Your machine Full
Relay No, ciphertext only Any cheap VPS / container Minimal
Applier Yes, it holds the session Destination machine Full

Compromising the relay yields ciphertext and metadata, not sessions. The AES key is derived from a transfer code that never travels with the package.

Interface

Popup: capture and preview Popup: transfer code
Popup Ready

Settings

Features

Cookies come through chrome.cookies, so HttpOnly and Secure values are included and Chrome's App-Bound Encryption never gets in the way. Everything origin-scoped (localStorage, sessionStorage, IndexedDB, Cache Storage) is captured on demand by injecting a self-contained function into matching tabs. There is no content script and no page-facing API.

IndexedDB uses a tagged structured-clone serializer, so Date, Blob, ArrayBuffer, typed arrays, Map and Set survive the round trip. JSON.stringify would have turned most of those into empty objects without complaining.

Packages are tamper-evident: format, version, alg, createdAt, expiresAt, transferId, target and domains are bound as AES-GCM additional authenticated data, so re-targeting a package breaks decryption. A package also dies after five minutes, enforced independently by the extension and the relay.

Where a transfer is incomplete, the extension says so. Partitioned (CHIPS) cookies are dropped and counted. Origins with no open tab are listed as gaps rather than quietly skipped. After applying, the applier closes the context to force Chromium's cookie flush and then reads the cookies back; a failed verification is reported as a failure, never as success.

What cannot be transferred

State Why
Passkeys / WebAuthn Hardware- or platform-bound; not extractable by any extension.
Device-bound sessions (e.g. Google DBSC) Bound to a device key at the TLS/session layer. Replay is rejected by design.
Service worker / in-memory JS state Ephemeral, not addressable.
Partitioned (CHIPS) cookies Cannot be replayed outside their top-level site. Dropped and counted.

The extension states these in-product instead of pretending the transfer is complete.

Install

Full instructions: docs/INSTALL.md

git clone https://github.com/rayss868/session-bridge.git
cd session-bridge

# 1. relay (stdlib only)
python3 -c "import secrets; print(secrets.token_urlsafe(32))" > relay.token
chmod 600 relay.token
python3 relay/relay.py --port 8787 --store ./data/packages --token-file ./relay.token

# 2. extension
#    chrome://extensions -> Developer mode -> Load unpacked -> select extension/

# 3. applier
python3 -m pip install --user cryptography

Usage

# 1. In the extension popup: enter domains, Preview, Send to relay.
#    You get a package id and a transfer code.
#
# 2. On the destination machine, with the code from a separate channel:

python3 tools/apply.py \
  --relay https://relay.example.com \
  --id '<package-id>' \
  --relay-token "$SESSION_BRIDGE_TOKEN" \
  --code 'XXXX-XXXX-XXXX-XXXX-XXXX-XXXX' \
  --mcp-url https://<host>/mcp \
  --mcp-token "$CLOAK_MCP_TOKEN" \
  --sandbox <sandbox-name> \
  --takeover \
  --filename session-state.json \
  --verify-url https://www.facebook.com/ \
  --allow-wipe

--allow-wipe is mandatory because browser_set_storage_state replaces the entire remote profile. Export every domain you care about in one package, or use one profile per account. See Two rules that bite.

No relay? Use Copy package in the popup and --file instead of --relay.

The IP problem (read this before blaming the tool)

Copying cookies across an IP change is the textbook session-hijacking signal. Meta properties in particular answer with a checkpoint instead of a session. The transfer mechanism is not the weak link. The exit IP is.

CloakBrowser supports this natively:

PLAYWRIGHT_MCP_PROXY_SERVER=http://user:pass@host:port
PLAYWRIGHT_MCP_PROXY_BYPASS=localhost,127.0.0.1
CLOAK_PLAYWRIGHT_MCP_GEOIP_PROXY_MATCH=true

geoip: true aligns timezone and locale with the proxy's exit IP rather than the machine's. Measured behaviour on a real Indonesian proxy exit:

{"ok": true, "country": "ID", "city": "Rembangan",
 "timezone": "Asia/Jakarta", "language": "id-ID", "webdriver": false}

Two practical notes:

  • The geoip resolver has a 20 second default timeout and resolves the exit IP through the proxy first. Slow residential proxies exceed it, so set CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS=60.
  • Free public proxies do not work for Meta. In testing, 50 of 1,093 Indonesian candidates were alive for ipinfo.io, but Instagram and Facebook both died at the tunnel (ERR_TIMED_OUT), while Instagram loaded fine from the same machine with no proxy. Use a paid residential/mobile proxy in the source country.

Verify any proxy before trusting it:

node tools/proxy-probe.mjs 'http://user:pass@host:port'
node tools/geo-ig-probe.mjs 'http://user:pass@host:port'

Two rules that bite

1. browser_set_storage_state replaces the whole profile. It clears every cookie and localStorage entry first. Applying an Instagram-only package deletes Facebook, TikTok, and everything else in that profile. apply.py refuses to run without --allow-wipe for exactly this reason.

2. Cookies flush to disk only on a clean context close. Before browser_close, the profile's Cookies SQLite database has zero rows for the new cookies. apply.py always closes, then verifies by reading back. If verification cannot confirm the cookies, the run is a failure.

Security model

  • AES-256-GCM, key = PBKDF2-HMAC-SHA256(transfer code, random 16-byte salt, 310 000 iterations, 32 bytes). Fresh key and IV per export.
  • Header bound as AAD, so origin/expiry/target tampering fails decryption.
  • Relay requires a bearer token on every route except /api/health, compares it in constant time, stores ciphertext only, refuses any package whose alg is not AES-256-GCM, enforces its own TTL, and never returns package bodies in the list endpoint.
  • The extension renders counts, never values. redactForLog() scrubs value-bearing fields before anything is logged.
  • No content scripts, no externally_connectable, no page-triggerable API.

Threats this does not solve: a compromised source machine, a malicious extension with cookie permissions, clipboard history, and the fact that a package plus its code is a complete credential. Details: SECURITY.md.

Project layout

extension/                Manifest V3 extension, no build step, load unpacked
  src/core/session.js       pure logic: cookie mapping, storageState, crypto
  src/core/collector.js     page-context read/write, injected on demand
  test/session.test.mjs     20 unit tests
  test/e2e-browser.mjs      real-Chromium E2E over CDP
relay/                    stdlib-only Python relay + Dockerfile
tools/apply.py            decrypt, stage, apply via MCP, verify
tools/proxy-probe.mjs     does traffic really exit through this proxy?
docs/ARCHITECTURE.md      internals and failure modes
docs/PROTOCOL.md          normative package format and relay API
docs/INSTALL.md           step-by-step setup

Tests

cd extension && node --test test/session.test.mjs   # 20 pass
python3 relay/test_relay.py                          # 13 pass
node extension/test/e2e-browser.mjs                  # real Chromium

The E2E script needs a Chromium that honours --load-extension. Branded Chrome headless builds (150) ignore it; the CloakBrowser Chromium build works, and so does any desktop Chrome/Chromium.

Verified end-to-end

Measured 2026-10-25 against a real remote CloakBrowser instance, not a mock:

  • Extension service worker started and the popup rendered in Chromium 146.
  • A live preview read a real cookie and a real localStorage key from a page.
  • The exported package contained no plaintext cookie value.
  • Relay round-trip: store → fetch → decrypt → apply.
  • After apply, the remote profile's Cookies SQLite database contained the HttpOnly cookie, and its LevelDB log contained the localStorage value.
  • State survived a restart of the MCP unit.
  • Docker image: /api/health 200, unauthenticated POST 401, authenticated POST 201, container healthcheck healthy.

License

MIT. See LICENSE.

Disclaimer

Use this only with accounts you own. Moving a session between machines may violate a service's terms of use, and replayed sessions are frequently flagged by anti-fraud systems. You are responsible for how you use it.

About

Encrypted browser session transfer: MV3 extension + ciphertext-only relay + Playwright MCP applier. Move HttpOnly cookies and localStorage to a remote browser without a third party seeing the tokens.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages