@@ -5,6 +5,7 @@ import type { ServerOptions as HttpsServerOptions } from 'node:https'
5
5
import { createServer as createHttpsServer } from 'node:https'
6
6
import type { Socket } from 'node:net'
7
7
import type { Duplex } from 'node:stream'
8
+ import crypto from 'node:crypto'
8
9
import colors from 'picocolors'
9
10
import type { WebSocket as WebSocketRaw } from 'ws'
10
11
import { WebSocketServer as WebSocketServerRaw_ } from 'ws'
@@ -87,6 +88,29 @@ function noop() {
87
88
// noop
88
89
}
89
90
91
+ // we only allow websockets to be connected if it has a valid token
92
+ // this is to prevent untrusted origins to connect to the server
93
+ // for example, Cross-site WebSocket hijacking
94
+ //
95
+ // we should check the token before calling wss.handleUpgrade
96
+ // otherwise untrusted ws clients will be included in wss.clients
97
+ //
98
+ // using the query params means the token might be logged out in server or middleware logs
99
+ // but we assume that is not an issue since the token is regenerated for each process
100
+ function hasValidToken ( config : ResolvedConfig , url : URL ) {
101
+ const token = url . searchParams . get ( 'token' )
102
+ if ( ! token ) return false
103
+
104
+ try {
105
+ const isValidToken = crypto . timingSafeEqual (
106
+ Buffer . from ( token ) ,
107
+ Buffer . from ( config . webSocketToken ) ,
108
+ )
109
+ return isValidToken
110
+ } catch { } // an error is thrown when the length is incorrect
111
+ return false
112
+ }
113
+
90
114
export function createWebSocketServer (
91
115
server : HttpServer | null ,
92
116
config : ResolvedConfig ,
@@ -116,7 +140,6 @@ export function createWebSocketServer(
116
140
}
117
141
}
118
142
119
- let wss : WebSocketServerRaw_
120
143
let wsHttpServer : Server | undefined = undefined
121
144
122
145
const hmr = isObject ( config . server . hmr ) && config . server . hmr
@@ -135,23 +158,64 @@ export function createWebSocketServer(
135
158
const port = hmrPort || 24678
136
159
const host = ( hmr && hmr . host ) || undefined
137
160
161
+ const shouldHandle = ( req : IncomingMessage ) => {
162
+ const protocol = req . headers [ 'sec-websocket-protocol' ] !
163
+ // vite-ping is allowed to connect from anywhere
164
+ // because it needs to be connected before the client fetches the new `/@vite/client`
165
+ // this is fine because vite-ping does not receive / send any meaningful data
166
+ if ( protocol === 'vite-ping' ) return true
167
+
168
+ if ( config . legacy ?. skipWebSocketTokenCheck ) {
169
+ return true
170
+ }
171
+
172
+ // If the Origin header is set, this request might be coming from a browser.
173
+ // Browsers always sets the Origin header for WebSocket connections.
174
+ if ( req . headers . origin ) {
175
+ const parsedUrl = new URL ( `http://example.com${ req . url ! } ` )
176
+ return hasValidToken ( config , parsedUrl )
177
+ }
178
+
179
+ // We allow non-browser requests to connect without a token
180
+ // for backward compat and convenience
181
+ // This is fine because if you can sent a request without the SOP limitation,
182
+ // you can also send a normal HTTP request to the server.
183
+ return true
184
+ }
185
+ const handleUpgrade = (
186
+ req : IncomingMessage ,
187
+ socket : Duplex ,
188
+ head : Buffer ,
189
+ isPing : boolean ,
190
+ ) => {
191
+ wss . handleUpgrade ( req , socket as Socket , head , ( ws ) => {
192
+ // vite-ping is allowed to connect from anywhere
193
+ // we close the connection immediately without connection event
194
+ // so that the client does not get included in `wss.clients`
195
+ if ( isPing ) {
196
+ ws . close ( /* Normal Closure */ 1000 )
197
+ return
198
+ }
199
+ wss . emit ( 'connection' , ws , req )
200
+ } )
201
+ }
202
+ const wss : WebSocketServerRaw_ = new WebSocketServerRaw ( { noServer : true } )
203
+ wss . shouldHandle = shouldHandle
204
+
138
205
if ( wsServer ) {
139
206
let hmrBase = config . base
140
207
const hmrPath = hmr ? hmr . path : undefined
141
208
if ( hmrPath ) {
142
209
hmrBase = path . posix . join ( hmrBase , hmrPath )
143
210
}
144
- wss = new WebSocketServerRaw ( { noServer : true } )
145
211
hmrServerWsListener = ( req , socket , head ) => {
212
+ const protocol = req . headers [ 'sec-websocket-protocol' ] !
213
+ const parsedUrl = new URL ( `http://example.com${ req . url ! } ` )
146
214
if (
147
- [ HMR_HEADER , 'vite-ping' ] . includes (
148
- req . headers [ 'sec-websocket-protocol' ] ! ,
149
- ) &&
150
- req . url === hmrBase
215
+ [ HMR_HEADER , 'vite-ping' ] . includes ( protocol ) &&
216
+ parsedUrl . pathname === hmrBase
151
217
) {
152
- wss . handleUpgrade ( req , socket as Socket , head , ( ws ) => {
153
- wss . emit ( 'connection' , ws , req )
154
- } )
218
+ handleUpgrade ( req , socket as Socket , head , protocol === 'vite-ping' )
155
219
}
156
220
}
157
221
wsServer . on ( 'upgrade' , hmrServerWsListener )
@@ -177,7 +241,6 @@ export function createWebSocketServer(
177
241
} else {
178
242
wsHttpServer = createHttpServer ( route )
179
243
}
180
- wss = new WebSocketServerRaw ( { noServer : true } )
181
244
wsHttpServer . on ( 'upgrade' , ( req , socket , head ) => {
182
245
const protocol = req . headers [ 'sec-websocket-protocol' ] !
183
246
if ( protocol === 'vite-ping' && server && ! server . listening ) {
@@ -187,9 +250,7 @@ export function createWebSocketServer(
187
250
req . destroy ( )
188
251
return
189
252
}
190
- wss . handleUpgrade ( req , socket as Socket , head , ( ws ) => {
191
- wss . emit ( 'connection' , ws , req )
192
- } )
253
+ handleUpgrade ( req , socket as Socket , head , protocol === 'vite-ping' )
193
254
} )
194
255
wsHttpServer . on ( 'error' , ( e : Error & { code : string } ) => {
195
256
if ( e . code === 'EADDRINUSE' ) {
@@ -207,9 +268,6 @@ export function createWebSocketServer(
207
268
}
208
269
209
270
wss . on ( 'connection' , ( socket ) => {
210
- if ( socket . protocol === 'vite-ping' ) {
211
- return
212
- }
213
271
socket . on ( 'message' , ( raw ) => {
214
272
if ( ! customListeners . size ) return
215
273
let parsed : any
0 commit comments