File tree Expand file tree Collapse file tree 4 files changed +81
-1
lines changed
packages/vite/src/node/server
playground/fs-serve/__tests__ Expand file tree Collapse file tree 4 files changed +81
-1
lines changed Original file line number Diff line number Diff line change @@ -102,6 +102,7 @@ import type { DevEnvironment } from './environment'
102
102
import { hostValidationMiddleware } from './middlewares/hostCheck'
103
103
import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest'
104
104
import { memoryFilesMiddleware } from './middlewares/memoryFiles'
105
+ import { rejectNoCorsRequestMiddleware } from './middlewares/rejectNoCorsRequest'
105
106
106
107
const usedConfigs = new WeakSet < ResolvedConfig > ( )
107
108
@@ -864,8 +865,8 @@ export async function _createServer(
864
865
middlewares . use ( timeMiddleware ( root ) )
865
866
}
866
867
867
- // disallows request that contains `#` in the URL
868
868
middlewares . use ( rejectInvalidRequestMiddleware ( ) )
869
+ middlewares . use ( rejectNoCorsRequestMiddleware ( ) )
869
870
870
871
// cors
871
872
const { cors } = serverConfig
Original file line number Diff line number Diff line change 1
1
import type { Connect } from 'dep-types/connect'
2
2
3
+ // disallows request that contains `#` in the URL
3
4
export function rejectInvalidRequestMiddleware ( ) : Connect . NextHandleFunction {
4
5
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
5
6
return function viteRejectInvalidRequestMiddleware ( req , res , next ) {
Original file line number Diff line number Diff line change
1
+ import type { Connect } from 'dep-types/connect'
2
+
3
+ /**
4
+ * A middleware that rejects no-cors mode requests that are not same-origin.
5
+ *
6
+ * We should avoid untrusted sites to load the script to avoid attacks like GHSA-4v9v-hfq4-rm2v.
7
+ * This is because:
8
+ * - the path of HMR patch files / entry point files can be predictable
9
+ * - the HMR patch files may not include ESM syntax
10
+ * (if they include ESM syntax, loading as a classic script would fail)
11
+ * - the HMR runtime in the browser has the list of all loaded modules
12
+ *
13
+ * https://github.com/webpack/webpack-dev-server/security/advisories/GHSA-4v9v-hfq4-rm2v
14
+ * https://green.sapphi.red/blog/local-server-security-best-practices#_2-using-xssi-and-modifying-the-prototype
15
+ * https://green.sapphi.red/blog/local-server-security-best-practices#properly-check-the-request-origin
16
+ */
17
+ export function rejectNoCorsRequestMiddleware ( ) : Connect . NextHandleFunction {
18
+ // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
19
+ return function viteRejectNoCorsRequestMiddleware ( req , res , next ) {
20
+ // While we can set Cross-Origin-Resource-Policy header instead of rejecting requests,
21
+ // we choose to reject the request to be safer in case the request handler has any side-effects.
22
+ if (
23
+ req . headers [ 'sec-fetch-mode' ] === 'no-cors' &&
24
+ req . headers [ 'sec-fetch-site' ] !== 'same-origin'
25
+ ) {
26
+ res . statusCode = 403
27
+ res . end ( 'Cross-origin requests must be made with CORS mode enabled.' )
28
+ return
29
+ }
30
+ return next ( )
31
+ }
32
+ }
Original file line number Diff line number Diff line change @@ -560,3 +560,49 @@ describe.runIf(!isServe)('preview HTML', () => {
560
560
. toBe ( '404' )
561
561
} )
562
562
} )
563
+
564
+ test . runIf ( isServe ) (
565
+ 'load script with no-cors mode from a different origin' ,
566
+ async ( ) => {
567
+ const viteTestUrlUrl = new URL ( viteTestUrl )
568
+
569
+ // NOTE: fetch cannot be used here as `fetch` sets some headers automatically
570
+ const res = await new Promise < http . IncomingMessage > ( ( resolve , reject ) => {
571
+ http
572
+ . get (
573
+ viteTestUrl + '/src/code.js' ,
574
+ {
575
+ headers : {
576
+ 'Sec-Fetch-Dest' : 'script' ,
577
+ 'Sec-Fetch-Mode' : 'no-cors' ,
578
+ 'Sec-Fetch-Site' : 'same-site' ,
579
+ Origin : 'http://vite.dev' ,
580
+ Host : viteTestUrlUrl . host ,
581
+ } ,
582
+ } ,
583
+ ( res ) => {
584
+ resolve ( res )
585
+ } ,
586
+ )
587
+ . on ( 'error' , ( e ) => {
588
+ reject ( e )
589
+ } )
590
+ } )
591
+ expect ( res . statusCode ) . toBe ( 403 )
592
+ const body = Buffer . concat ( await ArrayFromAsync ( res ) ) . toString ( )
593
+ expect ( body ) . toBe (
594
+ 'Cross-origin requests must be made with CORS mode enabled.' ,
595
+ )
596
+ } ,
597
+ )
598
+
599
+ // Note: Array.fromAsync is only supported in Node.js 22+
600
+ async function ArrayFromAsync < T > (
601
+ asyncIterable : AsyncIterable < T > ,
602
+ ) : Promise < T [ ] > {
603
+ const chunks = [ ]
604
+ for await ( const chunk of asyncIterable ) {
605
+ chunks . push ( chunk )
606
+ }
607
+ return chunks
608
+ }
You can’t perform that action at this time.
0 commit comments