feat: improve cookie import robustness and cross-platform support (#736)

This commit is contained in:
Jinwoo Hong 2026-04-16 23:44:10 -04:00 committed by GitHub
parent e8e5258138
commit 961f507ed3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 999 additions and 169 deletions

File diff suppressed because it is too large Load diff

View file

@ -7,9 +7,9 @@ import {
pickCookieFile,
importCookiesFromFile,
detectInstalledBrowsers,
selectBrowserProfile,
importCookiesFromBrowser
} from '../browser/browser-cookie-import'
import type { DetectedBrowser } from '../browser/browser-cookie-import'
import type {
BrowserSetGrabModeArgs,
BrowserSetGrabModeResult,
@ -313,18 +313,36 @@ export function registerBrowserHandlers(): void {
ipcMain.removeHandler('browser:session:detectBrowsers')
ipcMain.removeHandler('browser:session:importFromBrowser')
ipcMain.handle('browser:session:detectBrowsers', (event): DetectedBrowser[] => {
if (!isTrustedBrowserRenderer(event.sender)) {
return []
ipcMain.handle(
'browser:session:detectBrowsers',
(
event
): {
family: string
label: string
profiles: { name: string; directory: string }[]
selectedProfile: string
}[] => {
if (!isTrustedBrowserRenderer(event.sender)) {
return []
}
// Why: the renderer only needs family/label/profiles for the UI picker.
// Strip cookiesPath, keychainService, and keychainAccount to avoid
// exposing filesystem paths and credential store identifiers to the renderer.
return detectInstalledBrowsers().map((b) => ({
family: b.family,
label: b.label,
profiles: b.profiles,
selectedProfile: b.selectedProfile
}))
}
return detectInstalledBrowsers()
})
)
ipcMain.handle(
'browser:session:importFromBrowser',
async (
event,
args: { profileId: string; browserFamily: string }
args: { profileId: string; browserFamily: string; browserProfile?: string }
): Promise<BrowserCookieImportResult> => {
if (!isTrustedBrowserRenderer(event.sender)) {
return { ok: false, reason: 'Not authorized' }
@ -334,17 +352,43 @@ export function registerBrowserHandlers(): void {
return { ok: false, reason: 'Session profile not found.' }
}
// Why: browserProfile comes from the renderer and is used to construct
// a filesystem path. Reject traversal characters to prevent a compromised
// renderer from reading arbitrary files via the cookie import pipeline.
if (
args.browserProfile &&
(/[/\\]/.test(args.browserProfile) || args.browserProfile.includes('..'))
) {
return { ok: false, reason: 'Invalid browser profile name.' }
}
const browsers = detectInstalledBrowsers()
const browser = browsers.find((b) => b.family === args.browserFamily)
let browser = browsers.find((b) => b.family === args.browserFamily)
if (!browser) {
return { ok: false, reason: 'Browser not found on this system.' }
}
// Why: if the user selected a non-default profile from the picker,
// resolve the cookies path for that specific profile.
if (args.browserProfile && args.browserProfile !== browser.selectedProfile) {
const reselected = selectBrowserProfile(browser, args.browserProfile)
if (!reselected) {
return {
ok: false,
reason: `No cookies database found for profile "${args.browserProfile}".`
}
}
browser = reselected
}
const result = await importCookiesFromBrowser(browser, profile.partition)
if (result.ok) {
const profileName =
browser.profiles.find((p) => p.directory === browser.selectedProfile)?.name ??
browser.selectedProfile
browserSessionRegistry.updateProfileSource(args.profileId, {
browserFamily: browser.family,
profileName: 'Default',
profileName,
importedAt: Date.now()
})
return { ...result, profileId: args.profileId }

View file

@ -133,13 +133,21 @@ export type BrowserApi = {
sessionImportFromBrowser: (args: {
profileId: string
browserFamily: string
browserProfile?: string
}) => Promise<BrowserCookieImportResult>
sessionClearDefaultCookies: () => Promise<boolean>
}
export type DetectedBrowserProfileInfo = {
name: string
directory: string
}
export type DetectedBrowserInfo = {
family: BrowserSessionProfileSource['browserFamily']
label: string
profiles: DetectedBrowserProfileInfo[]
selectedProfile: string
}
export type PreflightStatus = {

View file

@ -85,11 +85,17 @@ export type BrowserSlice = {
deleteBrowserSessionProfile: (profileId: string) => Promise<boolean>
importCookiesToProfile: (profileId: string) => Promise<BrowserCookieImportResult>
clearBrowserSessionImportState: () => void
detectedBrowsers: { family: string; label: string }[]
detectedBrowsers: {
family: string
label: string
profiles: { name: string; directory: string }[]
selectedProfile: string
}[]
fetchDetectedBrowsers: () => Promise<void>
importCookiesFromBrowser: (
profileId: string,
browserFamily: string
browserFamily: string,
browserProfile?: string
) => Promise<BrowserCookieImportResult>
clearDefaultSessionCookies: () => Promise<boolean>
browserUrlHistory: BrowserHistoryEntry[]
@ -1159,6 +1165,8 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
const browsers = (await window.api.browser.sessionDetectBrowsers()) as {
family: string
label: string
profiles: { name: string; directory: string }[]
selectedProfile: string
}[]
set({ detectedBrowsers: browsers })
} catch {
@ -1166,7 +1174,7 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
}
},
importCookiesFromBrowser: async (profileId, browserFamily) => {
importCookiesFromBrowser: async (profileId, browserFamily, browserProfile?) => {
set({
browserSessionImportState: {
profileId,
@ -1178,7 +1186,8 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
try {
const result = (await window.api.browser.sessionImportFromBrowser({
profileId,
browserFamily
browserFamily,
browserProfile
})) as BrowserCookieImportResult
if (result.ok) {
set({

View file

@ -181,7 +181,7 @@ export type BrowserTab = BrowserWorkspace
export type BrowserSessionProfileScope = 'default' | 'isolated' | 'imported'
export type BrowserSessionProfileSource = {
browserFamily: 'chrome' | 'chromium' | 'arc' | 'edge' | 'manual'
browserFamily: 'chrome' | 'chromium' | 'arc' | 'edge' | 'firefox' | 'safari' | 'manual'
profileName?: string
importedAt: number
}