Skip to content

Commit 36a97b9

Browse files
eps1lonztanner
authored andcommitted
Allow blocking cross-site dev-only websocket connections from privacy-sensitive origins (#62)
1 parent 93c3993 commit 36a97b9

File tree

3 files changed

+72
-21
lines changed

3 files changed

+72
-21
lines changed

packages/next/src/server/lib/router-server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { normalizedAssetPrefix } from '../../shared/lib/normalized-asset-prefix'
4848
import { NEXT_PATCH_SYMBOL } from './patch-fetch'
4949
import type { ServerInitResult } from './render-server'
5050
import { filterInternalHeaders } from './server-ipc/utils'
51-
import { blockCrossSite } from './router-utils/block-cross-site'
51+
import { blockCrossSiteDEV } from './router-utils/block-cross-site-dev'
5252
import { traceGlobals } from '../../trace/shared'
5353
import { NoFallbackError } from '../../shared/lib/no-fallback-error.external'
5454
import {
@@ -345,7 +345,7 @@ export async function initialize(opts: {
345345
// handle hot-reloader first
346346
if (development) {
347347
if (
348-
blockCrossSite(
348+
blockCrossSiteDEV(
349349
req,
350350
res,
351351
development.config.allowedDevOrigins,
@@ -802,7 +802,7 @@ export async function initialize(opts: {
802802

803803
if (opts.dev && development && req.url) {
804804
if (
805-
blockCrossSite(
805+
blockCrossSiteDEV(
806806
req,
807807
socket,
808808
development.config.allowedDevOrigins,

packages/next/src/server/lib/router-utils/block-cross-site.ts renamed to packages/next/src/server/lib/router-utils/block-cross-site-dev.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function warnOrBlockRequest(
3131
return true
3232
}
3333

34-
function isInternalDevEndpoint(req: IncomingMessage): boolean {
34+
function isInternalEndpoint(req: IncomingMessage): boolean {
3535
if (!req.url) return false
3636

3737
try {
@@ -50,7 +50,7 @@ function isInternalDevEndpoint(req: IncomingMessage): boolean {
5050
}
5151
}
5252

53-
export const blockCrossSite = (
53+
export const blockCrossSiteDEV = (
5454
req: IncomingMessage,
5555
res: ServerResponse | Duplex,
5656
allowedDevOrigins: string[] | undefined,
@@ -70,9 +70,10 @@ export const blockCrossSite = (
7070
}
7171

7272
// only process internal URLs/middleware
73-
if (!isInternalDevEndpoint(req)) {
73+
if (!isInternalEndpoint(req)) {
7474
return false
7575
}
76+
7677
// block non-cors request from cross-site e.g. script tag on
7778
// different host
7879
if (
@@ -82,20 +83,20 @@ export const blockCrossSite = (
8283
return warnOrBlockRequest(res, undefined, mode)
8384
}
8485

85-
// ensure websocket requests from allowed origin
86+
// ensure websocket requests are only fulfilled from allowed origin
8687
const rawOrigin = req.headers['origin']
87-
88-
if (rawOrigin && rawOrigin !== 'null') {
89-
const parsedOrigin = parseUrl(rawOrigin)
90-
91-
if (parsedOrigin) {
92-
const originLowerCase = parsedOrigin.hostname.toLowerCase()
93-
94-
if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) {
95-
return warnOrBlockRequest(res, originLowerCase, mode)
96-
}
97-
}
98-
}
99-
100-
return false
88+
const parsedOrigin =
89+
rawOrigin && rawOrigin !== 'null' ? parseUrl(rawOrigin) : rawOrigin
90+
91+
const originLowerCase =
92+
parsedOrigin === undefined || typeof parsedOrigin === 'string'
93+
? parsedOrigin
94+
: parsedOrigin.hostname.toLowerCase()
95+
96+
// Allow requests with no origin since those are just GET requests from same-site
97+
return (
98+
originLowerCase !== undefined &&
99+
!isCsrfOriginAllowed(originLowerCase, allowedOrigins) &&
100+
warnOrBlockRequest(res, originLowerCase, mode)
101+
)
101102
}

test/development/basic/allowed-dev-origins.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,56 @@ describe.each([['', '/docs']])(
361361
server.close()
362362
}
363363
})
364+
365+
it('blocks cross-site requests from privacy-sensitive origins', async () => {
366+
const server = http.createServer((req, res) => {
367+
res.appendHeader('Content-Security-Policy', 'sandbox allow-scripts')
368+
res.end(`
369+
<html>
370+
<head>
371+
<title>testing cross-site privacy-sensitive</title>
372+
</head>
373+
<body>
374+
<script>
375+
(() => {
376+
const statusEl = document.createElement('p')
377+
statusEl.id = 'status'
378+
document.querySelector('body').appendChild(statusEl)
379+
380+
const ws = new WebSocket("${next.url}/_next/webpack-hmr")
381+
382+
ws.addEventListener('error', (err) => {
383+
statusEl.innerText = 'error'
384+
})
385+
ws.addEventListener('open', () => {
386+
statusEl.innerText = 'connected'
387+
})
388+
})()
389+
</script>
390+
</body>
391+
</html>
392+
`)
393+
})
394+
395+
const port = await findPort()
396+
await new Promise<void>((res) => {
397+
server.listen(port, () => res())
398+
})
399+
400+
try {
401+
const browser = await webdriver(`http://127.0.0.1:${port}`, '/')
402+
403+
await retry(async () => {
404+
expect(await browser.elementByCss('#status').text()).toBe('error')
405+
})
406+
} finally {
407+
await new Promise<void>((res) => {
408+
server.close(() => {
409+
res()
410+
})
411+
})
412+
}
413+
})
364414
})
365415
}
366416
)

0 commit comments

Comments
 (0)