Skip to content

Commit 785de5e

Browse files
graydawncclaude
andauthored
fix: implement RFC 6265 domain matching in Chrome cookies capability (#84)
The previous implementation built a `LIKE '%.${host}'` pattern, which only matched cookie host_keys that end in the full request hostname. Cookies set with an explicit parent Domain attribute (e.g. `.reddit.com`) were missed when the request URL used a subdomain (`https://www.reddit.com`), because `.reddit.com` does not end in `.www.reddit.com`. Replace with proper RFC 6265 §5.1.3 matching: enumerate all valid host_key values for a given request host (self host-only, self with leading dot, each parent domain with leading dot — stopping before bare TLDs) and query with `IN (...)`. Verified live against Chrome cookie DB: - https://reddit.com → 10 cookies, reddit_session present - https://www.reddit.com → 14 cookies, reddit_session present (includes extra host-only cookies on www.reddit.com, correctly excluded from the apex query) Co-authored-by: Chen <99816898+donteatfriedrice@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a62a9ec commit 785de5e

2 files changed

Lines changed: 86 additions & 7 deletions

File tree

packages/core/src/connectors/capabilities/cookies-chrome.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,60 @@
11
import { describe, it, expect } from 'vitest'
2-
import { makeChromeCookiesCapability } from './cookies-chrome.js'
2+
import { makeChromeCookiesCapability, getMatchingHostKeys } from './cookies-chrome.js'
33
import { SyncError, SyncErrorCode } from '@spool/connector-sdk'
44

5+
describe('getMatchingHostKeys', () => {
6+
it('matches host-only and same-host domain cookies', () => {
7+
expect(getMatchingHostKeys('reddit.com')).toEqual([
8+
'reddit.com',
9+
'.reddit.com',
10+
])
11+
})
12+
13+
it('matches parent domain cookies for subdomain requests', () => {
14+
expect(getMatchingHostKeys('www.reddit.com')).toEqual([
15+
'www.reddit.com',
16+
'.www.reddit.com',
17+
'.reddit.com',
18+
])
19+
})
20+
21+
it('walks all parent labels for deep subdomains', () => {
22+
expect(getMatchingHostKeys('a.b.example.co.uk')).toEqual([
23+
'a.b.example.co.uk',
24+
'.a.b.example.co.uk',
25+
'.b.example.co.uk',
26+
'.example.co.uk',
27+
'.co.uk',
28+
])
29+
})
30+
31+
it('does not walk into a bare TLD', () => {
32+
const keys = getMatchingHostKeys('reddit.com')
33+
expect(keys).not.toContain('.com')
34+
expect(keys).not.toContain('com')
35+
})
36+
37+
it('lower-cases the input host', () => {
38+
expect(getMatchingHostKeys('WWW.Reddit.COM')).toEqual([
39+
'www.reddit.com',
40+
'.www.reddit.com',
41+
'.reddit.com',
42+
])
43+
})
44+
45+
it('strips a leading dot from the input', () => {
46+
expect(getMatchingHostKeys('.reddit.com')).toEqual([
47+
'reddit.com',
48+
'.reddit.com',
49+
])
50+
})
51+
52+
it('returns empty for single-label or empty hosts', () => {
53+
expect(getMatchingHostKeys('localhost')).toEqual([])
54+
expect(getMatchingHostKeys('')).toEqual([])
55+
})
56+
})
57+
558
describe('makeChromeCookiesCapability', () => {
659
it('returns a capability with a get method', () => {
760
const cap = makeChromeCookiesCapability()

packages/core/src/connectors/capabilities/cookies-chrome.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,33 @@ interface RawCookieFull {
123123
is_httponly: string
124124
}
125125

126-
function queryAllCookiesForDomain(
126+
/**
127+
* Enumerate every Chrome `host_key` value that should match a request to `host`
128+
* per RFC 6265 §5.1.3. Chrome stores host-only cookies under the bare hostname
129+
* and domain cookies under `.parent.example.com`; a request to `www.example.com`
130+
* must see cookies at `www.example.com`, `.www.example.com`, and `.example.com`
131+
* but not anything scoped to a sibling (`.other.example.com`) or a TLD alone.
132+
*/
133+
export function getMatchingHostKeys(host: string): string[] {
134+
const normalized = host.toLowerCase().replace(/^\./, '')
135+
if (!normalized || !normalized.includes('.')) return []
136+
137+
const keys = [normalized, `.${normalized}`]
138+
let cur = normalized
139+
while (true) {
140+
const idx = cur.indexOf('.')
141+
if (idx < 0) break
142+
const parent = cur.substring(idx + 1)
143+
if (!parent.includes('.')) break
144+
keys.push(`.${parent}`)
145+
cur = parent
146+
}
147+
return keys
148+
}
149+
150+
function queryAllCookiesForHost(
127151
dbPath: string,
128-
domain: string,
152+
host: string,
129153
): { cookies: RawCookieFull[]; dbVersion: number } {
130154
if (!existsSync(dbPath)) {
131155
throw new SyncError(
@@ -134,9 +158,12 @@ function queryAllCookiesForDomain(
134158
)
135159
}
136160

137-
const safeDomain = domain.replace(/'/g, "''")
161+
const keys = getMatchingHostKeys(host)
162+
if (keys.length === 0) return { cookies: [], dbVersion: 0 }
163+
164+
const quoted = keys.map(k => `'${k.replace(/'/g, "''")}'`).join(',')
138165
// Fetch cookies and DB version in one sqlite3 invocation to avoid double process spawn
139-
const sql = `SELECT name, host_key, path, hex(encrypted_value) as encrypted_value_hex, value, expires_utc, is_secure, is_httponly, (SELECT value FROM meta WHERE key='version') as db_version FROM cookies WHERE host_key LIKE '%${safeDomain}';`
166+
const sql = `SELECT name, host_key, path, hex(encrypted_value) as encrypted_value_hex, value, expires_utc, is_secure, is_httponly, (SELECT value FROM meta WHERE key='version') as db_version FROM cookies WHERE host_key IN (${quoted});`
140167

141168
const output = runSqliteQuery(dbPath, sql)
142169

@@ -190,8 +217,7 @@ export function makeChromeCookiesCapability(): CookiesCapability {
190217
const key = getMacOSChromeKey()
191218

192219
const host = domainFromUrl(query.url)
193-
const dotHost = host.startsWith('.') ? host : `.${host}`
194-
const result = queryAllCookiesForDomain(dbPath, dotHost)
220+
const result = queryAllCookiesForHost(dbPath, host)
195221

196222
const cookies: Cookie[] = []
197223
for (const raw of result.cookies) {

0 commit comments

Comments
 (0)