Skip to content

Commit f52fb89

Browse files
committed
Port normalization and null-safety check
1 parent bf90130 commit f52fb89

File tree

2 files changed

+61
-9
lines changed

2 files changed

+61
-9
lines changed

src/__tests__/unit/checks/keywords-urls.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,44 @@ describe('urls guardrail', () => {
384384
expect(result.info?.blocked).toContain('help-suntropy.es');
385385
expect(result.info?.blocked).toContain('help.suntropy.es');
386386
});
387+
388+
it('treats explicit default ports as equivalent to no port', async () => {
389+
// URLs with explicit default ports should match allow list entries without ports
390+
const config = {
391+
url_allow_list: ['example.com'],
392+
allowed_schemes: new Set(['https', 'http']),
393+
allow_subdomains: false,
394+
block_userinfo: true,
395+
};
396+
397+
const text = 'Visit https://example.com:443 and http://example.com:80';
398+
const result = await urls({}, text, config);
399+
400+
// Both should be allowed (443 is default for https, 80 is default for http)
401+
expect(result.tripwireTriggered).toBe(false);
402+
expect(result.info?.allowed).toContain('https://example.com:443');
403+
expect(result.info?.allowed).toContain('http://example.com:80');
404+
expect(result.info?.blocked).toEqual([]);
405+
});
406+
407+
it('blocks non-default ports when allow list has no port', async () => {
408+
// URLs with non-default ports should be blocked when allow list doesn't specify a port
409+
const config = {
410+
url_allow_list: ['example.com'],
411+
allowed_schemes: new Set(['https']),
412+
allow_subdomains: false,
413+
block_userinfo: true,
414+
};
415+
416+
const text = 'Visit https://example.com:8443 and https://example.com:9000';
417+
const result = await urls({}, text, config);
418+
419+
// Both should be allowed - when allow list has no port, any port is OK
420+
// (This matches the behavior: no port restriction when not specified)
421+
expect(result.tripwireTriggered).toBe(false);
422+
expect(result.info?.allowed).toContain('https://example.com:8443');
423+
expect(result.info?.allowed).toContain('https://example.com:9000');
424+
});
387425
});
388426

389427
describe('competitors guardrail', () => {

src/checks/urls.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
390390

391391
const allowedScheme = hasExplicitScheme ? parsedAllowed.protocol.replace(/:$/, '').toLowerCase() : '';
392392
const allowedPort = safeGetPort(parsedAllowed, allowedScheme);
393-
const allowIndicatesPort = parsedAllowed.host.includes(':') && !parsedAllowed.host.startsWith('[');
393+
const allowIndicatesPort = Boolean(parsedAllowed.host) && parsedAllowed.host.includes(':') && !parsedAllowed.host.startsWith('[');
394394
if (allowedPort === null && allowIndicatesPort) {
395395
continue;
396396
}
@@ -410,10 +410,17 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
410410
continue;
411411
}
412412

413-
// Port matching: only enforce when allow list entry explicitly specifies a port
414-
// Check parsedAllowed.port (empty string when no port specified) not allowedPort (always has default)
415-
if (parsedAllowed.port) {
416-
if (urlPort === null || allowedPort !== urlPort) {
413+
// 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) {
417424
continue;
418425
}
419426
}
@@ -448,10 +455,17 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
448455

449456
const allowedDomain = allowedHost.replace(/^www\./, '');
450457

451-
// Port matching: only enforce when allow list entry explicitly specifies a port
452-
// Check parsedAllowed.port (empty string when no port specified) not allowedPort (always has default)
453-
if (parsedAllowed.port) {
454-
if (urlPort === null || allowedPort !== urlPort) {
458+
// 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) {
455469
continue;
456470
}
457471
}

0 commit comments

Comments
 (0)