@@ -75,6 +75,46 @@ interface InspectorProps {
75
75
name ?: string ;
76
76
}
77
77
78
+ const ALLOWED_HOST_HOSTNAMES = [ "127.0.0.1" , "[::1]" , "localhost" ] ;
79
+ const ALLOWED_ORIGIN_HOSTNAMES = [
80
+ "devtools.devprod.cloudflare.dev" ,
81
+ "cloudflare-devtools.pages.dev" ,
82
+ / ^ [ a - z 0 - 9 ] + \. c l o u d f l a r e - d e v t o o l s \. p a g e s \. d e v $ / ,
83
+ "127.0.0.1" ,
84
+ "[::1]" ,
85
+ "localhost" ,
86
+ ] ;
87
+ function validateWebSocketHandshake ( req : IncomingMessage ) {
88
+ // Validate `Host` header
89
+ const hostHeader = req . headers . host ;
90
+ if ( hostHeader == null ) return false ;
91
+ try {
92
+ const host = new URL ( `http://${ hostHeader } ` ) ;
93
+ if ( ! ALLOWED_HOST_HOSTNAMES . includes ( host . hostname ) ) return false ;
94
+ } catch {
95
+ return false ;
96
+ }
97
+ // Validate `Origin` header
98
+ let originHeader = req . headers . origin ;
99
+ if ( originHeader === null && req . headers [ "user-agent" ] === undefined ) {
100
+ // VSCode doesn't send an `Origin` header, but also doesn't send a
101
+ // `User-Agent` header, so allow an empty origin in this case.
102
+ originHeader = "http://localhost" ;
103
+ }
104
+ if ( originHeader == null ) return false ;
105
+ try {
106
+ const origin = new URL ( originHeader ) ;
107
+ const allowed = ALLOWED_ORIGIN_HOSTNAMES . some ( ( rule ) => {
108
+ if ( typeof rule === "string" ) return origin . hostname === rule ;
109
+ else return rule . test ( origin . hostname ) ;
110
+ } ) ;
111
+ if ( ! allowed ) return false ;
112
+ } catch {
113
+ return false ;
114
+ }
115
+ return true ;
116
+ }
117
+
78
118
export default function useInspector ( props : InspectorProps ) {
79
119
/** A unique ID for this session. */
80
120
const inspectorIdRef = useRef ( randomId ( ) ) ;
@@ -110,7 +150,7 @@ export default function useInspector(props: InspectorProps) {
110
150
case "/json/list" :
111
151
{
112
152
res . setHeader ( "Content-Type" , "application/json" ) ;
113
- const localHost = `localhost :${ props . port } /ws` ;
153
+ const localHost = `127.0.0.1 :${ props . port } /ws` ;
114
154
const devtoolsFrontendUrl = `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=${ localHost } ` ;
115
155
const devtoolsFrontendUrlCompat = `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${ localHost } ` ;
116
156
res . end (
@@ -155,7 +195,12 @@ export default function useInspector(props: InspectorProps) {
155
195
}
156
196
const wsServer = wsServerRef . current ;
157
197
158
- wsServer . on ( "connection" , ( ws : WebSocket ) => {
198
+ wsServer . on ( "connection" , ( ws : WebSocket , req : IncomingMessage ) => {
199
+ if ( ! validateWebSocketHandshake ( req ) ) {
200
+ ws . close ( 1008 , "Unauthorised" ) ;
201
+ return ;
202
+ }
203
+
159
204
if ( wsServer . clients . size > 1 ) {
160
205
/** We only want to have one active Devtools instance at a time. */
161
206
logger . error (
@@ -197,7 +242,7 @@ export default function useInspector(props: InspectorProps) {
197
242
timeout : 2000 ,
198
243
abortSignal : abortController . signal ,
199
244
} ) ;
200
- server . listen ( props . port ) ;
245
+ server . listen ( props . port , "127.0.0.1" ) ;
201
246
}
202
247
startInspectorProxy ( ) . catch ( ( err ) => {
203
248
if ( ( err as { code : string } ) . code !== "ABORT_ERR" ) {
@@ -844,7 +889,7 @@ export const openInspector = async (
844
889
) => {
845
890
const query = new URLSearchParams ( ) ;
846
891
query . set ( "theme" , "systemPreferred" ) ;
847
- query . set ( "ws" , `localhost :${ inspectorPort } /ws` ) ;
892
+ query . set ( "ws" , `127.0.0.1 :${ inspectorPort } /ws` ) ;
848
893
if ( worker ) query . set ( "domain" , worker ) ;
849
894
const url = `https://devtools.devprod.cloudflare.dev/js_app?${ query . toString ( ) } ` ;
850
895
const errorMessage =
0 commit comments