@@ -167,7 +167,173 @@ export function isHttp(path: string) {
167
167
return false ;
168
168
}
169
169
}
170
+ /**
171
+ * Determines whether the given url is an unsafe or internal url.
172
+ *
173
+ * @param path - The URL or path to check
174
+ * @returns true if the URL is unsafe/internal, false otherwise
175
+ */
176
+ export function isUnsafeUrl ( path : string ) : boolean {
177
+ if ( ! path || typeof path !== "string" ) {
178
+ return true ;
179
+ }
180
+
181
+ // Trim whitespace and convert to lowercase for comparison
182
+ const normalizedPath = path . trim ( ) . toLowerCase ( ) ;
183
+
184
+ // Empty or just whitespace
185
+ if ( ! normalizedPath ) {
186
+ return true ;
187
+ }
188
+
189
+ // JavaScript protocols
190
+ if (
191
+ normalizedPath . startsWith ( "javascript:" ) ||
192
+ normalizedPath . startsWith ( "vbscript:" ) ||
193
+ normalizedPath . startsWith ( "data:" )
194
+ ) {
195
+ return true ;
196
+ }
170
197
198
+ // File protocol
199
+ if ( normalizedPath . startsWith ( "file:" ) ) {
200
+ return true ;
201
+ }
202
+
203
+ // Local/internal network addresses
204
+ const localPatterns = [
205
+ // Localhost variations
206
+ "localhost" ,
207
+ "127.0.0.1" ,
208
+ "::1" ,
209
+
210
+ // Private IP ranges (RFC 1918)
211
+ "10." ,
212
+ "172.16." ,
213
+ "172.17." ,
214
+ "172.18." ,
215
+ "172.19." ,
216
+ "172.20." ,
217
+ "172.21." ,
218
+ "172.22." ,
219
+ "172.23." ,
220
+ "172.24." ,
221
+ "172.25." ,
222
+ "172.26." ,
223
+ "172.27." ,
224
+ "172.28." ,
225
+ "172.29." ,
226
+ "172.30." ,
227
+ "172.31." ,
228
+ "192.168." ,
229
+
230
+ // Link-local addresses
231
+ "169.254." ,
232
+
233
+ // Internal domains
234
+ ".local" ,
235
+ ".internal" ,
236
+ ".intranet" ,
237
+ ".corp" ,
238
+ ".home" ,
239
+ ".lan" ,
240
+ ] ;
241
+
242
+ try {
243
+ // Try to parse as URL
244
+ const url = new URL ( normalizedPath . startsWith ( "//" ) ? "http:" + normalizedPath : normalizedPath ) ;
245
+
246
+ const hostname = url . hostname . toLowerCase ( ) ;
247
+
248
+ // Check against local patterns
249
+ for ( const pattern of localPatterns ) {
250
+ if ( hostname === pattern || hostname . startsWith ( pattern ) || hostname . endsWith ( pattern ) ) {
251
+ return true ;
252
+ }
253
+ }
254
+
255
+ // Check for IP addresses in private ranges
256
+ if ( isPrivateIP ( hostname ) ) {
257
+ return true ;
258
+ }
259
+
260
+ // Check for non-standard ports that might indicate internal services
261
+ const port = url . port ;
262
+ if ( port && isInternalPort ( parseInt ( port ) ) ) {
263
+ return true ;
264
+ }
265
+ } catch ( e ) {
266
+ // If URL parsing fails, check if it's a relative path or contains suspicious patterns
267
+
268
+ // Relative paths starting with / are generally safe for same-origin
269
+ if ( normalizedPath . startsWith ( "/" ) && ! normalizedPath . startsWith ( "//" ) ) {
270
+ return false ;
271
+ }
272
+
273
+ // Check for localhost patterns in non-URL strings
274
+ for ( const pattern of localPatterns ) {
275
+ if ( normalizedPath . includes ( pattern ) ) {
276
+ return true ;
277
+ }
278
+ }
279
+ }
280
+
281
+ return false ;
282
+ }
283
+
284
+ /**
285
+ * Helper function to check if an IP address is in a private range
286
+ */
287
+ function isPrivateIP ( ip : string ) : boolean {
288
+ const ipRegex = / ^ ( \d { 1 , 3 } ) \. ( \d { 1 , 3 } ) \. ( \d { 1 , 3 } ) \. ( \d { 1 , 3 } ) $ / ;
289
+ const match = ip . match ( ipRegex ) ;
290
+
291
+ if ( ! match ) {
292
+ return false ;
293
+ }
294
+
295
+ const [ , a , b , c , d ] = match . map ( Number ) ;
296
+
297
+ // Validate IP format
298
+ if ( a > 255 || b > 255 || c > 255 || d > 255 ) {
299
+ return false ;
300
+ }
301
+
302
+ // Private IP ranges
303
+ return (
304
+ a === 10 || a === 127 || ( a === 172 && b >= 16 && b <= 31 ) || ( a === 192 && b === 168 ) || ( a === 169 && b === 254 ) // Link-local
305
+ ) ;
306
+ }
307
+
308
+ /**
309
+ * Helper function to check if a port is typically used for internal services
310
+ */
311
+ function isInternalPort ( port : number ) : boolean {
312
+ const internalPorts = [
313
+ 22 , // SSH
314
+ 23 , // Telnet
315
+ 25 , // SMTP
316
+ 53 , // DNS
317
+ 135 , // RPC
318
+ 139 , // NetBIOS
319
+ 445 , // SMB
320
+ 993 , // IMAPS
321
+ 995 , // POP3S
322
+ 1433 , // SQL Server
323
+ 1521 , // Oracle
324
+ 3306 , // MySQL
325
+ 3389 , // RDP
326
+ 5432 , // PostgreSQL
327
+ 5900 , // VNC
328
+ 6379 , // Redis
329
+ 8080 , // Common internal web
330
+ 8443 , // Common internal HTTPS
331
+ 9200 , // Elasticsearch
332
+ 27017 , // MongoDB
333
+ ] ;
334
+
335
+ return internalPorts . includes ( port ) ;
336
+ }
171
337
/**
172
338
* Determines whether the given path is a filesystem path.
173
339
* This includes "file://" URLs.
0 commit comments