@@ -14,7 +14,7 @@ const DEFAULT_PORTS: Record<string, number> = {
1414 https : 443 ,
1515} ;
1616
17- const SCHEME_PREFIX_RE = / ^ [ a - z ] [ a - z 0 - 9 + . - ] * : \/ \/ / i ;
17+ const SCHEME_PREFIX_RE = / ^ [ a - z ] [ a - z 0 - 9 + . - ] * : \/ \/ / ;
1818const HOSTLESS_SCHEMES = new Set ( [ 'data' , 'javascript' , 'vbscript' , 'mailto' ] ) ;
1919
2020function normalizeAllowedSchemes ( value : unknown ) : Set < string > {
@@ -30,7 +30,7 @@ function normalizeAllowedSchemes(value: unknown): Set<string> {
3030 } else if ( Array . isArray ( value ) ) {
3131 rawValues = value ;
3232 } else {
33- throw new Error ( 'allowed_schemes entries must be strings ' ) ;
33+ throw new Error ( 'allowed_schemes must be a string, Set, or Array ' ) ;
3434 }
3535
3636 const normalized = new Set < string > ( ) ;
@@ -331,6 +331,44 @@ function isIpv4Address(value: string): boolean {
331331 }
332332}
333333
334+ /**
335+ * Check if port matching should block the URL.
336+ *
337+ * Only enforces port matching when the allow list entry explicitly specifies
338+ * a non-default port. Explicit default ports (e.g., :443 for https) are
339+ * treated as equivalent to no port being specified.
340+ *
341+ * @param urlPort - The URL's port number (or default for its scheme)
342+ * @param urlParsed - The parsed URL object
343+ * @param allowedPort - The allow list entry's port number (or default for its scheme)
344+ * @param allowedParsed - The parsed allow list entry URL object
345+ * @param urlScheme - The URL's scheme
346+ * @param allowedScheme - The allow list entry's scheme
347+ * @returns true if the port doesn't match and should be blocked, false otherwise
348+ */
349+ function shouldBlockDueToPortMismatch (
350+ urlPort : number | null ,
351+ urlParsed : URL ,
352+ allowedPort : number | null ,
353+ allowedParsed : URL ,
354+ urlScheme : string ,
355+ allowedScheme : string
356+ ) : boolean {
357+ // Only enforce port matching when allow list entry explicitly specifies a non-default port
358+ const allowedHasNonDefaultPort = allowedParsed . port &&
359+ ( allowedPort !== DEFAULT_PORTS [ allowedScheme as keyof typeof DEFAULT_PORTS ] ) ;
360+
361+ if ( ! allowedHasNonDefaultPort ) {
362+ return false ; // No port restriction when allow list has no non-default port
363+ }
364+
365+ // Allow list has explicit non-default port, so URL must match exactly
366+ const urlHasNonDefaultPort = urlParsed . port &&
367+ ( urlPort !== DEFAULT_PORTS [ urlScheme as keyof typeof DEFAULT_PORTS ] ) ;
368+
369+ return ! urlHasNonDefaultPort || allowedPort !== urlPort ;
370+ }
371+
334372/**
335373 * Check if URL is allowed based on the allow list configuration.
336374 *
@@ -406,23 +444,13 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
406444 }
407445
408446 // Scheme matching for IPs: only enforce when BOTH allow list entry AND URL have explicit schemes
409- if ( hasExplicitScheme && hadScheme && allowedScheme && allowedScheme !== schemeLower ) {
447+ if ( hasExplicitScheme && hadScheme && allowedScheme !== schemeLower ) {
410448 continue ;
411449 }
412450
413451 // Port matching: only enforce when allow list entry explicitly specifies a non-default port
414- // Explicit default ports (e.g., :443 for https) should be treated as no port specified
415- const allowedHasNonDefaultPort = parsedAllowed . port &&
416- ( allowedPort !== DEFAULT_PORTS [ allowedScheme as keyof typeof DEFAULT_PORTS ] ) ;
417-
418- if ( allowedHasNonDefaultPort ) {
419- // Allow list has explicit non-default port, so URL must match exactly
420- const urlHasNonDefaultPort = parsedUrl . port &&
421- ( urlPort !== DEFAULT_PORTS [ schemeLower as keyof typeof DEFAULT_PORTS ] ) ;
422-
423- if ( ! urlHasNonDefaultPort || allowedPort !== urlPort ) {
424- continue ;
425- }
452+ if ( shouldBlockDueToPortMismatch ( urlPort , parsedUrl , allowedPort , parsedAllowed , schemeLower , allowedScheme ) ) {
453+ continue ;
426454 }
427455
428456 if ( ipToInt ( allowedHost ) === urlIpInt ) {
@@ -456,18 +484,8 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
456484 const allowedDomain = allowedHost . replace ( / ^ w w w \. / , '' ) ;
457485
458486 // Port matching: only enforce when allow list entry explicitly specifies a non-default port
459- // Explicit default ports (e.g., :443 for https) should be treated as no port specified
460- const allowedHasNonDefaultPort = parsedAllowed . port &&
461- ( allowedPort !== DEFAULT_PORTS [ allowedScheme as keyof typeof DEFAULT_PORTS ] ) ;
462-
463- if ( allowedHasNonDefaultPort ) {
464- // Allow list has explicit non-default port, so URL must match exactly
465- const urlHasNonDefaultPort = parsedUrl . port &&
466- ( urlPort !== DEFAULT_PORTS [ schemeLower as keyof typeof DEFAULT_PORTS ] ) ;
467-
468- if ( ! urlHasNonDefaultPort || allowedPort !== urlPort ) {
469- continue ;
470- }
487+ if ( shouldBlockDueToPortMismatch ( urlPort , parsedUrl , allowedPort , parsedAllowed , schemeLower , allowedScheme ) ) {
488+ continue ;
471489 }
472490
473491 const hostMatches =
@@ -477,10 +495,12 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
477495 }
478496
479497 // Scheme matching for domains: only enforce when BOTH allow list entry AND URL have explicit schemes
480- if ( hasExplicitScheme && hadScheme && allowedScheme && allowedScheme !== schemeLower ) {
498+ if ( hasExplicitScheme && hadScheme && allowedScheme !== schemeLower ) {
481499 continue ;
482500 }
483501
502+ // Path matching: only enforce when allow list entry explicitly specifies a non-root path
503+ // Note: Empty string ('') and root ('/') are both treated as "no path restriction"
484504 if ( allowedPath && allowedPath !== '/' ) {
485505 // Normalize trailing slashes to avoid double-slash issues when checking subpaths
486506 // e.g., if allowedPath is "/api/", we normalize to "/api" before adding "/"
0 commit comments