fix: validate chromium profile directories

This commit is contained in:
Neil
2026-05-30 05:40:28 -07:00
parent c858c6f89e
commit 1d9bc92f4a
2 changed files with 52 additions and 0 deletions
@@ -122,6 +122,40 @@ describe('detectInstalledBrowsers — Comet', () => {
expect(names).toEqual(['Personal', 'Research', 'Work'])
})
it('ignores Comet profile directories that escape the browser root', async () => {
vi.doMock('node:fs', async () => {
const actual = await vi.importActual<typeof fsModule>('node:fs')
return {
...actual,
existsSync: (p: string) => {
if (p.includes('Comet/Local State')) {
return true
}
if (p.includes('Application Support/Outside/Network/Cookies')) {
return true
}
return false
},
readFileSync: (p: string, enc?: string) => {
if (typeof p === 'string' && p.includes('Comet/Local State')) {
return JSON.stringify({
profile: {
info_cache: {
'../Outside': { name: 'Outside' }
}
}
})
}
return actual.readFileSync(p as never, enc as never)
}
}
})
const { detectInstalledBrowsers } = await import('./browser-cookie-import')
const detected = detectInstalledBrowsers()
expect(detected.find((b) => b.family === 'comet')).toBeUndefined()
})
it('skips Comet when the data directory exists but no Cookies DB is present', async () => {
vi.doMock('node:fs', async () => {
const actual = await vi.importActual<typeof fsModule>('node:fs')
+18
View File
@@ -173,6 +173,17 @@ function resolveCookiesPath(profileDir: string): string | null {
return null
}
function isSafeBrowserProfileDirectory(directory: string): boolean {
return (
directory.length > 0 &&
directory !== '.' &&
!directory.includes('\0') &&
!directory.includes('/') &&
!directory.includes('\\') &&
!directory.includes('..')
)
}
// Why: Chrome's Local State JSON contains profile.info_cache which maps profile
// directory names (e.g. "Default", "Profile 1") to metadata including the
// user-visible display name. This lets us show human-readable names in the picker.
@@ -190,6 +201,10 @@ function discoverProfiles(browserRoot: string): BrowserProfile[] {
}
const profiles: BrowserProfile[] = []
for (const [dir, info] of Object.entries(infoCache)) {
// Why: Local State is external metadata, but profile dirs become path segments.
if (!isSafeBrowserProfileDirectory(dir)) {
continue
}
const profileName = (info as { name?: string })?.name ?? dir
profiles.push({ name: profileName, directory: dir })
}
@@ -362,6 +377,9 @@ export function selectBrowserProfile(
browser: DetectedBrowser,
profileDirectory: string
): DetectedBrowser | null {
if (!isSafeBrowserProfileDirectory(profileDirectory)) {
return null
}
if (browser.family === 'firefox') {
const profilesRoot = firefoxProfilesRoot()
if (!profilesRoot) {