fix: require exact pairing deep link route (#4282)

This commit is contained in:
Neil 2026-05-31 10:55:36 -07:00 committed by GitHub
parent 93bba155de
commit 58f10fa2da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 27 deletions

View File

@ -24,4 +24,9 @@ describe('web pairing input', () => {
it('still parses legacy hash-form pairing URLs', () => {
expect(parseWebPairingInput(`orca://pair#${encodeOffer()}`)).toEqual(offer)
})
it('rejects orca URLs outside the exact pairing route', () => {
expect(parseWebPairingInput(`orca://pairing?code=${encodeOffer()}`)).toBeNull()
expect(parseWebPairingInput(`orca://pair-extra?code=${encodeOffer()}`)).toBeNull()
})
})

View File

@ -14,19 +14,9 @@ export function parseWebPairingInput(input: string): WebPairingOffer | null {
}
try {
if (trimmed.startsWith('orca://pair')) {
const queryIndex = trimmed.indexOf('?')
if (queryIndex !== -1) {
const query = trimmed.slice(queryIndex + 1).split('#')[0] ?? ''
const params = new URLSearchParams(query)
const code = params.get('code')
return code ? decodePairingPayload(code) : null
}
const hashIndex = trimmed.indexOf('#')
if (hashIndex === -1) {
return null
}
return decodePairingPayload(trimmed.slice(hashIndex + 1))
if (trimmed.toLowerCase().startsWith('orca://')) {
const code = extractPairingCodeFromUrl(trimmed)
return code ? decodePairingPayload(code) : null
}
return decodePairingPayload(trimmed)
} catch {
@ -92,6 +82,28 @@ function decodePairingPayload(base64url: string): WebPairingOffer | null {
}
}
function extractPairingCodeFromUrl(url: string): string | null {
let parsed: URL
try {
parsed = new URL(url)
} catch {
return null
}
// Why: prefix checks accepted routes like `orca://pairing?...`; only the
// pairing deep-link host may carry runtime auth material.
if (parsed.protocol !== 'orca:' || parsed.hostname !== 'pair') {
return null
}
if (parsed.pathname !== '' && parsed.pathname !== '/') {
return null
}
const code = parsed.searchParams.get('code')
if (code) {
return code
}
return parsed.hash ? parsed.hash.slice(1) || null : null
}
function base64UrlToBytes(value: string): Uint8Array {
const base64 = value.replace(/-/g, '+').replace(/_/g, '/')
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')

View File

@ -32,6 +32,15 @@ describe('pairing offer', () => {
expect(() => decodePairingOffer('https://example.com#abc')).toThrow('Invalid pairing URL')
})
it('rejects orca URLs outside the exact pairing route', () => {
const url = encodePairingOffer(offer)
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
expect(parsePairingCode(`orca://pairing?code=${code}`)).toBeNull()
expect(parsePairingCode(`orca://pair-extra?code=${code}`)).toBeNull()
expect(() => decodePairingOffer(`orca://pairing?code=${code}`)).toThrow('Invalid pairing URL')
})
it('rejects URLs without a pairing code', () => {
expect(() => decodePairingOffer('orca://pair')).toThrow('Invalid pairing URL')
})

View File

@ -34,23 +34,25 @@ export function decodePairingOffer(url: string): PairingOffer {
}
function extractPairingCodeFromUrl(url: string): string | null {
if (!url.startsWith('orca://pair')) {
let parsed: URL
try {
parsed = new URL(url)
} catch {
return null
}
const queryIndex = url.indexOf('?')
if (queryIndex !== -1) {
const query = url.slice(queryIndex + 1).split('#')[0] ?? ''
const params = new URLSearchParams(query)
const code = params.get('code')
if (code) {
return code
}
// Why: prefix checks accepted routes like `orca://pairing?...`; only the
// pairing deep-link host may carry runtime auth material.
if (parsed.protocol !== 'orca:' || parsed.hostname !== 'pair') {
return null
}
const hashIndex = url.indexOf('#')
if (hashIndex !== -1) {
return url.slice(hashIndex + 1) || null
if (parsed.pathname !== '' && parsed.pathname !== '/') {
return null
}
return null
const code = parsed.searchParams.get('code')
if (code) {
return code
}
return parsed.hash ? parsed.hash.slice(1) || null : null
}
// Why: accept either an `orca://pair?...` URL or the bare base64
@ -62,7 +64,7 @@ export function parsePairingCode(input: string): PairingOffer | null {
return null
}
try {
if (trimmed.startsWith('orca://pair')) {
if (trimmed.toLowerCase().startsWith('orca://')) {
return decodePairingOffer(trimmed)
}
return decodePairingBase64(trimmed)