fix: validate Chromium profile directories (#3674)

* fix: validate chromium profile directories

* test: cover unsafe browser profile selection

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-05-30 12:33:43 -07:00
committed by GitHub
co-authored by Orca Jinwoo-H
parent 8d67f9807c
commit dfcd0f44e0
2 changed files with 83 additions and 0 deletions
@@ -122,6 +122,71 @@ 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('rejects explicit Comet profile selections 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('Application Support/Outside/Network/Cookies')) {
return true
}
return false
}
}
})
const { selectBrowserProfile } = await import('./browser-cookie-import')
const selected = selectBrowserProfile(
{
family: 'comet',
label: 'Comet',
cookiesPath: '/Users/test/Library/Application Support/Comet/Default/Network/Cookies',
keychainService: 'Comet Safe Storage',
keychainAccount: 'Comet',
profiles: [{ name: 'Outside', directory: '../Outside' }],
selectedProfile: 'Default'
},
'../Outside'
)
expect(selected).toBeNull()
})
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) {