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:
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' })
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user