Skip to content

Commit 868f141

Browse files
authored
fix(bundled-dev): reject requests to HMR patch files in non potentially trustworthy origins (#22269)
1 parent 3ec9cda commit 868f141

File tree

5 files changed

+23
-131
lines changed

5 files changed

+23
-131
lines changed

packages/vite/src/node/server/environments/fullBundleEnvironment.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,16 @@ export class FullBundleDevEnvironment extends DevEnvironment {
339339
code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code,
340340
})
341341

342-
this.memoryFiles.set(hmrOutput.filename, { source: hmrOutput.code })
342+
this.memoryFiles.set(hmrOutput.filename, {
343+
// ensure that the generated hmr patch contains ESM syntax
344+
// this is to avoid attacks like GHSA-4v9v-hfq4-rm2v
345+
// https://github.com/webpack/webpack-dev-server/security/advisories/GHSA-4v9v-hfq4-rm2v
346+
// https://green.sapphi.red/blog/local-server-security-best-practices#_2-using-xssi-and-modifying-the-prototype
347+
// https://green.sapphi.red/blog/local-server-security-best-practices#properly-check-the-request-origin
348+
// we can also use `Cross-Origin Resource Policy` header instead of this
349+
// but we cannot use `Sec-Fetch-*` headers as they are only sent to potentially-trustworthy origins
350+
source: hmrOutput.code + '\n; export {}',
351+
})
343352
if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) {
344353
this.memoryFiles.set(hmrOutput.sourcemapFilename, {
345354
source: hmrOutput.sourcemap,

packages/vite/src/node/server/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ import type { DevEnvironment } from './environment'
107107
import { hostValidationMiddleware } from './middlewares/hostCheck'
108108
import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest'
109109
import { memoryFilesMiddleware } from './middlewares/memoryFiles'
110-
import { rejectNoCorsRequestMiddleware } from './middlewares/rejectNoCorsRequest'
111110

112111
const usedConfigs = new WeakSet<ResolvedConfig>()
113112

@@ -929,7 +928,6 @@ export async function _createServer(
929928
}
930929

931930
middlewares.use(rejectInvalidRequestMiddleware())
932-
middlewares.use(rejectNoCorsRequestMiddleware())
933931

934932
// cors
935933
const { cors } = serverConfig

packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

playground/fs-serve/__tests__/commonTests.ts

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -599,84 +599,3 @@ describe.runIf(!isServe)('preview HTML', () => {
599599
.toBe('')
600600
})
601601
})
602-
603-
test.runIf(isServe)(
604-
'load script with no-cors mode from a different origin',
605-
async () => {
606-
const viteTestUrlUrl = new URL(viteTestUrl)
607-
608-
// NOTE: fetch cannot be used here as `fetch` sets some headers automatically
609-
const res = await new Promise<http.IncomingMessage>((resolve, reject) => {
610-
http
611-
.get(
612-
viteTestUrl + '/src/code.js',
613-
{
614-
headers: {
615-
'Sec-Fetch-Dest': 'script',
616-
'Sec-Fetch-Mode': 'no-cors',
617-
'Sec-Fetch-Site': 'same-site',
618-
Origin: 'http://vite.dev',
619-
Host: viteTestUrlUrl.host,
620-
},
621-
},
622-
(res) => {
623-
resolve(res)
624-
},
625-
)
626-
.on('error', (e) => {
627-
reject(e)
628-
})
629-
})
630-
expect(res.statusCode).toBe(200)
631-
const body = Buffer.concat(await ArrayFromAsync(res)).toString()
632-
expect(body).toContain(
633-
'Cross-origin requests for classic scripts must be made with CORS mode enabled.',
634-
)
635-
},
636-
)
637-
638-
test.runIf(isServe)(
639-
'load image with no-cors mode from a different origin should be allowed',
640-
async () => {
641-
const viteTestUrlUrl = new URL(viteTestUrl)
642-
643-
// NOTE: fetch cannot be used here as `fetch` sets some headers automatically
644-
const res = await new Promise<http.IncomingMessage>((resolve, reject) => {
645-
http
646-
.get(
647-
viteTestUrl + '/src/code.js',
648-
{
649-
headers: {
650-
'Sec-Fetch-Dest': 'image',
651-
'Sec-Fetch-Mode': 'no-cors',
652-
'Sec-Fetch-Site': 'same-site',
653-
Origin: 'http://vite.dev',
654-
Host: viteTestUrlUrl.host,
655-
},
656-
},
657-
(res) => {
658-
resolve(res)
659-
},
660-
)
661-
.on('error', (e) => {
662-
reject(e)
663-
})
664-
})
665-
expect(res.statusCode).not.toBe(403)
666-
const body = Buffer.concat(await ArrayFromAsync(res)).toString()
667-
expect(body).not.toContain(
668-
'Cross-origin requests for classic scripts must be made with CORS mode enabled.',
669-
)
670-
},
671-
)
672-
673-
// Note: Array.fromAsync is only supported in Node.js 22+
674-
async function ArrayFromAsync<T>(
675-
asyncIterable: AsyncIterable<T>,
676-
): Promise<T[]> {
677-
const chunks = []
678-
for await (const chunk of asyncIterable) {
679-
chunks.push(chunk)
680-
}
681-
return chunks
682-
}

playground/hmr-full-bundle-mode/__tests__/hmr-full-bundle-mode.spec.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,22 @@ if (isBuild) {
9595
// BUNDLED -> GENERATING_HMR_PATCH -> BUNDLED
9696
test('generate hmr patch', async () => {
9797
await expect.poll(() => page.textContent('.hmr')).toBe('hello')
98+
const hmrPatchFileRes = page.waitForResponse(/\/hmr_patch_\d\.js$/)
9899
editFile('hmr.js', (code) =>
99100
code.replace("const foo = 'hello'", "const foo = 'hello1'"),
100101
)
101-
await expect.poll(() => page.textContent('.hmr')).toBe('hello1')
102-
103-
editFile('hmr.js', (code) =>
104-
code.replace("const foo = 'hello1'", "const foo = 'hello'"),
105-
)
102+
try {
103+
await expect.poll(() => page.textContent('.hmr')).toBe('hello1')
104+
105+
// ensure that the generated hmr patch contains ESM syntax
106+
// so that it's not possible to load it in a <script> tag without type="module"
107+
// which would allow cross origin reads
108+
expect(await (await hmrPatchFileRes).text()).toMatch(/export\s*\{\}/)
109+
} finally {
110+
editFile('hmr.js', (code) =>
111+
code.replace("const foo = 'hello1'", "const foo = 'hello'"),
112+
)
113+
}
106114
await expect.poll(() => page.textContent('.hmr')).toContain('hello')
107115
await expect.poll(() => page.textContent('.asset')).toMatch(assetUrl)
108116
})

0 commit comments

Comments
 (0)