Ctrl K
ComunidadGuías…

__tests__/guard.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * 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 });
  });
});