fix(liveshare-protocol): forward only the page's own origin

Matching a forwarded route on the pathname alone let any origin whose URL
carried `/api/present.host` or `/open-in-app/apps` reach the owner instead of
the destination it named. Resolve the request against the page base and forward
it only when the target origin is the page's own; every other request reaches
the fetch this carrier replaced.
This commit is contained in:
mektpoy committed 2026-10-23 16:13:34 +08:00
1 parent 0a0620abd5
commit 774e618112
2 files changed
+40 -3

No files matched your search

@@ -190,8 +190,15 @@ export function installViewerCarrier(options: ViewerCarrierOptions): void {
let pathname: string | undefined
let method = 'GET'
try {
const pageBase = (globalThis as { document?: { readonly baseURI: string } }).document?.baseURI
pathname = new URL(input instanceof Request ? input.url : String(input), pageBase).pathname
const page = globalThis as {
readonly document?: { readonly baseURI: string }
readonly location?: { readonly origin: string }
}
const pageOrigin = page.location?.origin
const target = new URL(input instanceof Request ? input.url : String(input), page.document?.baseURI)
// A request outside the page's own origin belongs to that origin: its path naming an owner
// route must not divert it from the fetch the page replaced.
pathname = pageOrigin !== undefined && target.origin === pageOrigin ? target.pathname : undefined
method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase()
} catch {
// A target this page base cannot resolve is not one of the forwarded routes.
@@ -559,6 +559,36 @@ describe('viewer carrier forwarded page routes', () => {
expect(direct).toHaveBeenCalledTimes(3)
})
it('leaves a cross-origin request that names a forwarded route to the page fetch', async () => {
const direct = directFetch()
expect(await (await globalThis.fetch('https://cdn.example/api/present.host')).text())
.toBe('direct:https://cdn.example/api/present.host')
expect(await (await globalThis.fetch('https://cdn.example/x/open-in-app/apps')).text())
.toBe('direct:https://cdn.example/x/open-in-app/apps')
expect(direct).toHaveBeenCalledTimes(2)
})
it('forwards an absolute request the page makes to its own origin', async () => {
const { posted } = carrier(() => json({ name: 'owner-host' }))
const origin = new URL(document.baseURI).origin
expect(await (await globalThis.fetch(`${origin}/relay/s/abc/api/present.host`)).json())
.toMatchObject({ name: 'owner-host' })
expect(posted[0]).toMatchObject({ op: 'call', path: '/api/present.host' })
})
it('leaves a relative route to the page fetch once the page base names another origin', async () => {
const direct = directFetch()
const base = document.createElement('base')
base.href = 'https://cdn.example/'
document.head.append(base)
try {
expect(await (await globalThis.fetch('api/present.host')).text()).toBe('direct:api/present.host')
expect(direct).toHaveBeenCalledTimes(1)
} finally {
base.remove()
}
})
it('forwards a hit whose page URL carries a query without passing it to the owner', async () => {
const { posted } = carrier(() => json({ name: 'owner-host', available: true, fileManager: 'finder' }))
expect(await (await globalThis.fetch('/relay/s/abc/api/present.host?x=1#frag')).json()).toMatchObject({ name: 'owner-host' })
@@ -626,7 +656,7 @@ describe('viewer carrier forwarded page routes', () => {
it('reads the target and method from a Request object', async () => {
const { posted } = carrier(() => json({ apps: ['finder'] }))
const response = await globalThis.fetch(new Request('https://relay.example/open-in-app/apps'))
const response = await globalThis.fetch(new Request(new URL('open-in-app/apps', document.baseURI)))
expect(await response.json()).toEqual({ apps: ['finder'] })
expect(posted[0]).toMatchObject({ op: 'call', path: '/open-in-app/apps' })
})