Skip to content

Commit ce6b65d

Browse files
committed
Better CIDR handling
1 parent 28cabf9 commit ce6b65d

File tree

2 files changed

+94
-21
lines changed

2 files changed

+94
-21
lines changed

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,58 @@ describe('urls guardrail', () => {
422422
expect(result.info?.allowed).toContain('https://example.com:8443');
423423
expect(result.info?.allowed).toContain('https://example.com:9000');
424424
});
425+
426+
it('accepts CIDR /0 with 0.0.0.0 network address', async () => {
427+
// 0.0.0.0/0 should match all IPs
428+
const config = {
429+
url_allow_list: ['0.0.0.0/0'],
430+
allowed_schemes: new Set(['https']),
431+
allow_subdomains: false,
432+
block_userinfo: true,
433+
};
434+
435+
const text = 'Visit https://1.2.3.4 and https://192.168.1.1';
436+
const result = await urls({}, text, config);
437+
438+
expect(result.tripwireTriggered).toBe(false);
439+
expect(result.info?.allowed).toContain('https://1.2.3.4');
440+
expect(result.info?.allowed).toContain('https://192.168.1.1');
441+
});
442+
443+
it('rejects CIDR /0 with non-zero network address', async () => {
444+
// 10.0.0.0/0 is ambiguous - /0 should only use 0.0.0.0
445+
const config = {
446+
url_allow_list: ['10.0.0.0/0'],
447+
allowed_schemes: new Set(['https']),
448+
allow_subdomains: false,
449+
block_userinfo: true,
450+
};
451+
452+
const text = 'Visit https://10.5.5.5 and https://192.168.1.1';
453+
const result = await urls({}, text, config);
454+
455+
// Should block both because 10.0.0.0/0 is invalid (emits warning)
456+
expect(result.tripwireTriggered).toBe(true);
457+
expect(result.info?.blocked).toContain('https://10.5.5.5');
458+
expect(result.info?.blocked).toContain('https://192.168.1.1');
459+
});
460+
461+
it('rejects invalid CIDR prefix values', async () => {
462+
// Test various invalid CIDR prefixes
463+
const config = {
464+
url_allow_list: ['10.0.0.0/33', '192.168.0.0/-1', '172.16.0.0/abc'],
465+
allowed_schemes: new Set(['https']),
466+
allow_subdomains: false,
467+
block_userinfo: true,
468+
};
469+
470+
const text = 'Visit https://10.5.5.5 and https://192.168.1.1 and https://172.16.1.1';
471+
const result = await urls({}, text, config);
472+
473+
// All should be blocked due to invalid CIDR configurations
474+
expect(result.tripwireTriggered).toBe(true);
475+
expect(result.info?.blocked).toHaveLength(3);
476+
});
425477
});
426478

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

src/checks/urls.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,47 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
407407
continue;
408408
}
409409

410+
// Handle CIDR notation before URL parsing
411+
// CIDR blocks like "10.0.0.0/8" should not be parsed as URLs
412+
const cidrMatch = normalizedEntry.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
413+
if (cidrMatch) {
414+
// Only match against IP URLs
415+
if (!urlIsIp || urlIpInt === null) {
416+
continue;
417+
}
418+
419+
const [, network, prefixStr] = cidrMatch;
420+
const prefix = Number(prefixStr);
421+
422+
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) {
423+
console.warn(`Warning: Invalid CIDR prefix in allow list: "${normalizedEntry}"`);
424+
continue;
425+
}
426+
427+
// Validate /0 must use 0.0.0.0 for clarity
428+
// Any other network address with /0 is ambiguous and likely a configuration error
429+
if (prefix === 0 && network !== '0.0.0.0') {
430+
console.warn(
431+
`Warning: CIDR /0 prefix must use 0.0.0.0, not "${network}". Entry: "${normalizedEntry}"`
432+
);
433+
continue;
434+
}
435+
436+
try {
437+
const networkInt = ipToInt(network);
438+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
439+
if ((networkInt & mask) === (urlIpInt & mask)) {
440+
return true;
441+
}
442+
} catch (error) {
443+
console.warn(
444+
`Warning: Invalid CIDR network address in allow list: "${normalizedEntry}" - ${error instanceof Error ? error.message : error}`
445+
);
446+
}
447+
448+
continue; // Skip URL parsing for CIDR entries
449+
}
450+
410451
const hasExplicitScheme = SCHEME_PREFIX_RE.test(normalizedEntry);
411452

412453
let parsedAllowed: URL;
@@ -453,31 +494,11 @@ function isUrlAllowed(parsedUrl: URL, allowList: string[], allowSubdomains: bool
453494
continue;
454495
}
455496

497+
// Exact IP match
456498
if (ipToInt(allowedHost) === urlIpInt) {
457499
return true;
458500
}
459501

460-
let networkSpec = allowedHost;
461-
if (allowedPath && allowedPath !== '/') {
462-
networkSpec = `${networkSpec}${allowedPath}`;
463-
}
464-
465-
if (networkSpec.includes('/')) {
466-
const [network, prefixStr] = networkSpec.split('/');
467-
const prefix = Number(prefixStr);
468-
if (Number.isInteger(prefix) && prefix >= 0 && prefix <= 32) {
469-
try {
470-
const networkInt = ipToInt(network);
471-
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
472-
if ((networkInt & mask) === (urlIpInt & mask)) {
473-
return true;
474-
}
475-
} catch {
476-
// Invalid CIDR entry; ignore.
477-
}
478-
}
479-
}
480-
481502
continue;
482503
}
483504

0 commit comments

Comments
 (0)