Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions browse/src/url-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ export const BLOCKED_METADATA_HOSTS = new Set([
]);

/**
* IPv6 prefixes to block (CIDR-style). Any address starting with these
* hex prefixes is rejected. Covers the full ULA range (fc00::/7 = fc00:: and fd00::).
* IPv6 prefixes to block (CIDR-style). ULA addresses cover fc00::/7 and
* link-local addresses cover fe80::/10.
*/
const BLOCKED_IPV6_PREFIXES = ['fc', 'fd'];
const BLOCKED_IPV6_PREFIXES = ['fc', 'fd', 'fe8', 'fe9', 'fea', 'feb'];

/**
* Check if an IPv6 address falls within a blocked prefix range.
* Handles the full ULA range (fc00::/7), not just the exact literal fd00::.
* Handles the full ULA range (fc00::/7) and link-local range (fe80::/10),
* not just exact literals like fd00:: or fe80::1.
* Only matches actual IPv6 addresses (must contain ':'), not hostnames
* like fd.example.com or fcustomer.com.
*/
Expand Down Expand Up @@ -90,9 +91,7 @@ async function resolvesToBlockedIp(hostname: string): Promise<boolean> {
const v6Check = resolve6(hostname).then(
(addresses) => addresses.some(addr => {
const normalized = addr.toLowerCase();
return BLOCKED_METADATA_HOSTS.has(normalized) || isBlockedIpv6(normalized) ||
// fe80::/10 is link-local — always block (covers all fe80:: addresses)
normalized.startsWith('fe80:');
return BLOCKED_METADATA_HOSTS.has(normalized) || isBlockedIpv6(normalized);
}),
() => false, // ENODATA / ENOTFOUND — no AAAA records, not a risk
);
Expand Down
4 changes: 4 additions & 0 deletions browse/test/url-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ describe('validateNavigationUrl', () => {
await expect(validateNavigationUrl('http://[fc00::]/')).rejects.toThrow(/cloud metadata/i);
});

it('blocks direct IPv6 link-local addresses', async () => {
await expect(validateNavigationUrl('http://[fe80::2]/')).rejects.toThrow(/cloud metadata/i);
});

it('does not block hostnames starting with fd (e.g. fd.example.com)', async () => {
await expect(validateNavigationUrl('https://fd.example.com/')).resolves.toBeUndefined();
});
Expand Down