__tests__/guard.test.js
Rama main · réplica de lectura del repositorio original.
/**
* What the browser is allowed to open.
*
* This is the test file that matters most in the service. A browser any customer can point at any
* URL is a server-side request forgery engine, and this one runs inside the cluster it would be
* attacking — one unguarded fetch of the metadata endpoint or the provisioner's internal API is
* worth more to an attacker than everything else here combined.
*
* The two cases that bypass a naive implementation both have tests below: a public hostname that
* RESOLVES to a private address, and a public URL that REDIRECTS to one.
*/
import { describe, it, expect } from 'vitest';
import { assertPublicUrl, isBlockedAddress } from '../src/guard.js';
// A resolver we control, so these tests never touch real DNS.
const resolverFor = (map) => ({
lookup: async (host) => {
if (!map[host]) { const e = new Error('ENOTFOUND'); e.code = 'ENOTFOUND'; throw e; }
return map[host].map((address) => ({ address, family: address.includes(':') ? 6 : 4 }));
},
});
describe('addresses that must never be reachable', () => {
it.each([
['127.0.0.1', 'loopback'],
['10.1.2.3', 'private class A'],
['172.16.5.4', 'private class B'],
['192.168.1.1', 'private class C'],
['169.254.169.254', 'cloud metadata — the one that hands out credentials'],
['100.64.1.1', 'carrier-grade NAT, where tailnets live'],
['0.0.0.0', 'this network'],
['::1', 'IPv6 loopback'],
['fe80::1', 'IPv6 link-local'],
['fd00::1', 'IPv6 unique local'],
['::ffff:169.254.169.254', 'IPv4-mapped metadata — the same attack in a different hat'],
])('blocks %s (%s)', (ip) => {
expect(isBlockedAddress(ip)).toBe(true);
});
it.each([['1.1.1.1'], ['93.184.216.34'], ['2606:4700:4700::1111']])('allows public %s', (ip) => {
expect(isBlockedAddress(ip)).toBe(false);
});
it('treats anything unparseable as blocked rather than as allowed', () => {
expect(isBlockedAddress('not-an-ip')).toBe(true);
expect(isBlockedAddress('')).toBe(true);
});
});
describe('assertPublicUrl', () => {
const dns = resolverFor({
'example.com': ['93.184.216.34'],
'evil.test': ['169.254.169.254'],
'sneaky.test': ['93.184.216.34', '10.0.0.5'],
});
it('allows an ordinary public site', async () => {
await expect(assertPublicUrl('https://example.com/page', { resolver: dns })).resolves.toMatchObject({ ok: true });
});
/* The bypass a hostname-only check misses entirely. */
it('blocks a public NAME that resolves to a private address', async () => {
await expect(assertPublicUrl('https://evil.test/', { resolver: dns })).rejects.toThrow(/private address/i);
});
/* One good answer does not make a name safe — the browser may connect to either. */
it('blocks a name that returns one public and one private address', async () => {
await expect(assertPublicUrl('https://sneaky.test/', { resolver: dns })).rejects.toThrow(/private address/i);
});
it('blocks a literal private address without consulting DNS at all', async () => {
const never = { lookup: async () => { throw new Error('DNS must not be used for a literal'); } };
await expect(assertPublicUrl('http://169.254.169.254/latest/meta-data/', { resolver: never }))
.rejects.toThrow(/private network/i);
});
it('blocks internal hostnames that /etc/hosts would happily resolve', async () => {
for (const u of [
'http://localhost:3000/',
'http://provisioner.agents-system.svc.cluster.local:3030/api/apps',
'http://metadata/computeMetadata/v1/',
'http://something.internal/',
]) {
await expect(assertPublicUrl(u, { resolver: dns })).rejects.toThrow(/internal/i);
}
});
it('refuses protocols that are not http(s)', async () => {
for (const u of ['file:///etc/passwd', 'ftp://example.com/x', 'chrome://settings']) {
await expect(assertPublicUrl(u, { resolver: dns })).rejects.toThrow(/only http and https|not a URL/i);
}
});
it('refuses a name that does not resolve, rather than letting the browser try', async () => {
await expect(assertPublicUrl('https://nothing-here.test/', { resolver: dns })).rejects.toThrow(/does not resolve/i);
});
it('carries a status so the API answers with the right code', async () => {
await expect(assertPublicUrl('https://evil.test/', { resolver: dns })).rejects.toMatchObject({ status: 403 });
await expect(assertPublicUrl('nonsense', { resolver: dns })).rejects.toMatchObject({ status: 400 });
});
});