console fill: never copy Google session cookies by default (device-bound); own profile in a Chrome started without automation switches
Google binds its session cookies to the device (Device Bound Session Credentials) and signs out sessions whose cookies look stolen, which can sign the owner out of their real Chrome. The default is now storebound's own profile per store; storebound starts Chrome itself (--user-data-dir, --remote-debugging-port=0, no --enable-automation, navigator.webdriver false) and attaches over CDP. --use-chrome-session google|apple is an explicit opt-in that warns. /signin/rejected is said plainly.
This commit is contained in:
parent
e0bef82b06
commit
0035931734
22 changed files with 456 additions and 156 deletions
|
|
@ -1,13 +1,16 @@
|
|||
// storebound console fill: which browser it uses. Pure parts (data dir, profile pick, file selection, cookie host
|
||||
// filter, DevToolsActivePort) and the session copy on throwaway folders (cleanup on success and on error). The
|
||||
// integration test sets a cookie on a local fixture site in a THROWAWAY Chrome profile, then opens the console browser
|
||||
// in copy mode from that profile and sees the cookie (and not the one of another host). It never touches the real
|
||||
// Chrome profile or a store site; it skips when Google Chrome is not installed or not on macOS.
|
||||
// filter, DevToolsActivePort, the opt-in flag, the browser plan, Google's rejected page, the launch switches) and the
|
||||
// session copy on throwaway folders (cleanup on success and on error). Safety: the default run never copies Google
|
||||
// (or Apple) cookies; the opt-in copy of Google cookies warns about device-bound sessions. Integration (real Google
|
||||
// Chrome, throwaway profiles, a local fixture site): the window storebound starts has navigator.webdriver false and no
|
||||
// automation switch on its real command line (chrome://version); the opt-in copy carries a cookie of the source
|
||||
// profile and drops another host's. It never touches the real Chrome profile or a store site; it skips when Google
|
||||
// Chrome is not installed or not on macOS.
|
||||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { chromeDataDir, CONSOLE_HOSTS, cookieApplies, copySession, devToolsEndpoint, filterCookies, NEVER_COPY, openConsoleBrowser, pickProfile, sessionFiles } from '../cli/console/session';
|
||||
import { APPLE_COPY_NOTE, AUTOMATION_SWITCHES, browserPlan, chromeDataDir, consoleChromeArgs, CONSOLE_HOSTS, cookieApplies, copySession, devToolsEndpoint, filterCookies, GOOGLE_COPY_WARNING, GOOGLE_REJECTED, googleRejected, NEVER_COPY, openConsoleBrowser, parseUseChromeSession, pickProfile, PROVIDER_HOSTS, sessionFiles } from '../cli/console/session';
|
||||
import { capped, CLOSE_TIMEOUT_MS, LAUNCH_TIMEOUT_MS } from './helpers/browser';
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'sb-session-test-'));
|
||||
|
|
@ -72,6 +75,67 @@ describe('pure parts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Google session safety (device-bound cookies)', () => {
|
||||
const none = { storeboundChrome: null, chrome: null, chromium: null };
|
||||
const copies = () => readdirSync(tmpdir()).filter(f => f.startsWith('storebound-chrome-')).length;
|
||||
test('--use-chrome-session: google | apple only; absent = no copy', () => {
|
||||
expect(parseUseChromeSession(undefined)).toEqual([]);
|
||||
expect(parseUseChromeSession('google')).toEqual(['google']);
|
||||
expect(parseUseChromeSession('Apple, google,apple')).toEqual(['apple', 'google']);
|
||||
expect(() => parseUseChromeSession(true)).toThrow('--use-chrome-session');
|
||||
expect(() => parseUseChromeSession('all')).toThrow('--use-chrome-session');
|
||||
});
|
||||
test('plan: no copy by default for either store; an opt-in copies only the form\'s own provider', () => {
|
||||
for (const store of ['play', 'apple'] as const) expect(browserPlan({ store, platform: 'darwin' }, true).copy).toBeNull();
|
||||
expect(browserPlan({ store: 'play', platform: 'darwin', useChromeSession: ['google'] }, true).copy).toBe('google');
|
||||
expect(browserPlan({ store: 'play', platform: 'darwin', useChromeSession: ['apple'] }, true).copy).toBeNull();
|
||||
expect(browserPlan({ store: 'apple', platform: 'darwin', useChromeSession: ['google'] }, true).copy).toBeNull();
|
||||
expect(browserPlan({ store: 'apple', platform: 'darwin', useChromeSession: ['apple'] }, true).copy).toBe('apple');
|
||||
expect(browserPlan({ store: 'play', platform: 'darwin', useChromeSession: ['google'], fresh: true }, true).copy).toBeNull();
|
||||
expect(browserPlan({ store: 'play', platform: 'linux', useChromeSession: ['google'] }, true).copy).toBeNull();
|
||||
// an Apple copy never carries a Google cookie and the other way round
|
||||
expect(PROVIDER_HOSTS.apple.some(h => h.includes('google'))).toBe(false);
|
||||
expect(PROVIDER_HOSTS.google.some(h => h.includes('apple'))).toBe(false);
|
||||
});
|
||||
test('the default run makes no copy of the owner\'s Chrome cookies (play and apple)', async () => {
|
||||
const src = await fakeChrome('chrome-default');
|
||||
for (const store of ['play', 'apple'] as const) {
|
||||
const said: string[] = [];
|
||||
const before = copies();
|
||||
const e = await openConsoleBrowser(scratch, { store, profileDir: tmp(`own-${store}`), say: m => said.push(m), chromeDir: src, platform: 'darwin', launchDeps: { installed: none } }).catch(x => x as Error);
|
||||
expect((e as Error).message).toContain('no usable browser'); // it went straight to storebound's own profile
|
||||
expect(said.join('\n')).not.toMatch(/copied|Chrome sign-in|WARNING/);
|
||||
expect(copies()).toBe(before);
|
||||
}
|
||||
});
|
||||
test('the opt-in Google copy warns about device-bound sessions first; the Apple one says its note', async () => {
|
||||
const src = await fakeChrome('chrome-optin');
|
||||
const said: string[] = [];
|
||||
await openConsoleBrowser(scratch, { store: 'play', useChromeSession: ['google'], profileDir: tmp('own-optin'), say: m => said.push(m), chromeDir: src, platform: 'darwin', launchDeps: { installed: none } }).catch(() => null);
|
||||
expect(said[0]).toBe(GOOGLE_COPY_WARNING);
|
||||
expect(GOOGLE_COPY_WARNING).toContain('sign you out of your real Chrome');
|
||||
expect(said.join('\n')).toContain('could not use your Chrome sign-in'); // no Google Chrome here: the copy is removed again
|
||||
const saidA: string[] = [];
|
||||
await openConsoleBrowser(scratch, { store: 'apple', useChromeSession: ['apple'], profileDir: tmp('own-optin-a'), say: m => saidA.push(m), chromeDir: src, platform: 'darwin', launchDeps: { installed: none } }).catch(() => null);
|
||||
expect(saidA[0]).toBe(APPLE_COPY_NOTE);
|
||||
});
|
||||
test('Google\'s rejected page is recognised (and only that page)', () => {
|
||||
for (const u of ['https://accounts.google.com/v3/signin/rejected?continue=x', 'https://accounts.google.com/signin/rejected', 'https://accounts.google.com/signin/v2/rejected']) expect(googleRejected(u)).toBe(true);
|
||||
for (const u of ['https://accounts.google.com/v3/signin/accountchooser', 'https://accounts.google.com/v3/signin/identifier', 'https://play.google.com/console/signin/rejected', 'https://example.com/v3/signin/rejected', 'not a url']) expect(googleRejected(u)).toBe(false);
|
||||
expect(GOOGLE_REJECTED).toBe('Google refused sign-in in this window (/signin/rejected). storebound uses a normal Chrome window; try again, or sign in at accounts.google.com first in this window.');
|
||||
});
|
||||
test('launch switches: no automation switch, webdriver off, session restore, a DevTools port the OS picks', () => {
|
||||
const a = consoleChromeArgs('/p');
|
||||
for (const s of AUTOMATION_SWITCHES) expect(a.some(x => x.startsWith(s))).toBe(false);
|
||||
expect(a).toContain('--disable-blink-features=AutomationControlled');
|
||||
expect(a).toContain('--restore-last-session');
|
||||
expect(a).toContain('--remote-debugging-port=0');
|
||||
expect(a).toContain('--user-data-dir=/p');
|
||||
expect(a.some(x => /user-agent/i.test(x))).toBe(false);
|
||||
expect(consoleChromeArgs('/p', { headless: true })).toContain('--headless=new');
|
||||
});
|
||||
});
|
||||
|
||||
describe('session copy', () => {
|
||||
test('copies only the session files, filters the cookies, cleans up', async () => {
|
||||
const src = await fakeChrome('chrome-a');
|
||||
|
|
@ -151,9 +215,10 @@ describe.skipIf(!hasChrome)('copy mode with real Chrome (throwaway profile, loca
|
|||
seed = null;
|
||||
const said: string[] = [];
|
||||
b = await openConsoleBrowser(join(import.meta.dir, '..'), {
|
||||
store: 'apple', profileDir: tmp('own-profile'), say: m => said.push(m), chromeDir: src, hosts: ['127.0.0.1'], headless: true, platform: 'darwin', launchTimeoutMs: LAUNCH_TIMEOUT_MS,
|
||||
store: 'apple', useChromeSession: ['apple'], profileDir: tmp('own-profile'), say: m => said.push(m), chromeDir: src, hosts: ['127.0.0.1'], headless: true, platform: 'darwin', launchTimeoutMs: LAUNCH_TIMEOUT_MS,
|
||||
});
|
||||
expect(b.mode).toBe('copy');
|
||||
expect(said[0]).toBe(APPLE_COPY_NOTE);
|
||||
expect(said.join('\n')).toContain('your Chrome sign-in');
|
||||
expect(said.join('\n')).not.toContain('sb_test');
|
||||
expect(b.tempDir && existsSync(b.tempDir)).toBe(true);
|
||||
|
|
@ -161,13 +226,45 @@ describe.skipIf(!hasChrome)('copy mode with real Chrome (throwaway profile, loca
|
|||
expect(await b.page.textContent('body')).toBe('cookie: yes');
|
||||
await b.page.goto(`${other}/check`);
|
||||
expect(await b.page.textContent('body')).toBe('cookie: no');
|
||||
await b.context.close();
|
||||
await b.close();
|
||||
await b.closed;
|
||||
expect(existsSync(b.tempDir!)).toBe(false);
|
||||
} finally {
|
||||
if (seed) await capped(seed.close(), CLOSE_TIMEOUT_MS);
|
||||
if (b && !b.isClosed()) { await capped(b.context.close(), CLOSE_TIMEOUT_MS); b.cleanup(); }
|
||||
if (b && !b.isClosed()) { await capped(b.close(), CLOSE_TIMEOUT_MS); b.cleanup(); }
|
||||
server.stop(true);
|
||||
}
|
||||
}, 2 * LAUNCH_TIMEOUT_MS); // bounded by the two launches; ~3 s on an idle machine
|
||||
});
|
||||
|
||||
describe.skipIf(!hasChrome)('the console window has no automation markers (real Chrome, throwaway profile, local page)', () => {
|
||||
test('navigator.webdriver is false and the real command line has no --enable-automation', async () => {
|
||||
const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => new Response(
|
||||
'<p id="r"></p><script>document.getElementById("r").textContent = JSON.stringify({ webdriver: navigator.webdriver, ua: navigator.userAgent })</script>',
|
||||
{ headers: { 'content-type': 'text/html' } }) });
|
||||
let b: any = null;
|
||||
try {
|
||||
b = await openConsoleBrowser(join(import.meta.dir, '..'), {
|
||||
store: 'play', profileDir: tmp('own-webdriver'), say: () => {}, headless: true, platform: 'darwin', chromeDir: tmp('no-chrome-here'), launchTimeoutMs: LAUNCH_TIMEOUT_MS,
|
||||
});
|
||||
expect(b.mode).toBe('profile');
|
||||
for (const s of AUTOMATION_SWITCHES) expect(b.args.some((x: string) => x.startsWith(s))).toBe(false);
|
||||
await b.page.goto(`http://127.0.0.1:${server.port}/`);
|
||||
const r = JSON.parse(await b.page.textContent('#r'));
|
||||
expect(r.webdriver).toBe(false);
|
||||
expect(r.ua).toContain('Chrome/');
|
||||
await b.page.goto('chrome://version');
|
||||
const cmd: string = await b.page.textContent('#command_line');
|
||||
expect(cmd).toContain('--user-data-dir=');
|
||||
expect(cmd).not.toContain('--enable-automation');
|
||||
expect(cmd).not.toContain('--remote-debugging-pipe');
|
||||
expect(cmd).toContain('--disable-blink-features=AutomationControlled');
|
||||
await b.close();
|
||||
await b.closed;
|
||||
expect(b.isClosed()).toBe(true);
|
||||
} finally {
|
||||
if (b && !b.isClosed()) await capped(b.close(), CLOSE_TIMEOUT_MS);
|
||||
server.stop(true);
|
||||
}
|
||||
}, 2 * LAUNCH_TIMEOUT_MS);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue