Skip to content

Commit 28cabf9

Browse files
committed
Address copilot comments
1 parent f52fb89 commit 28cabf9

File tree

1 file changed

+48
-28
lines changed

1 file changed

+48
-28
lines changed

src/checks/urls.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const DEFAULT_PORTS: Record<string, number> = {
1414
https: 443,
1515
};
1616

17-
const SCHEME_PREFIX_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
17+
const SCHEME_PREFIX_RE = /^[a-z][a-z0-9+.-]*:\/\//;
1818
const HOSTLESS_SCHEMES = new Set(['data', 'javascript', 'vbscript', 'mailto']);
1919

2020
function 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(/^www\./, '');
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

Comments
 (0)