/** * Image-proxy helper tests (offline — no network). * * Contract under test: * - sniffImage: magic-byte → Content-Type for jpeg/png/gif/webp/avif/heic/bmp, * null for HTML blockpages and junk (a hotlink guard returning 200 HTML * must never be served as an image). * - isSafeProxyTarget: public http(s) only — no loopback, RFC1918, link-local, * IPv6 private, or non-web schemes; our own site origin rejected. * - proxyImageUrl: remote http(s) → /images?url=... ; relative / data: / * our-own-origin URLs pass through untouched. * * Runner: `tsx tests/image-proxy.test.ts` → TAP-style, exit 1 on failure. */ import assert from 'node:assert/strict'; process.env.NEXT_PUBLIC_SITE_URL = 'https://technews.krisforbes.ca'; import { isSafeProxyTarget, proxyImageUrl, isPrivateHost, } from '@/lib/image-mapping'; import { mediaKey, sniffImage } from '@/lib/images'; let passed = 0; let failed = 0; function check(name: string, fn: () => void): void { try { fn(); passed += 1; console.log(`ok ${passed + failed} ${name}`); } catch (err) { failed += 1; console.log(`not ok ${passed + failed} ${name}`); console.log(` ${(err as Error).message}`); } } const b = (b64: string) => Buffer.from(b64, 'base64'); // Minimal valid magic headers (payload irrelevant to the sniff). const JPEG = b( '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoKBwYKDwMNDhgMEggRCwUNDAwTFBMSFBQUFxQVFRUUgAFMAAQHBgMCAwYHBgcKEA0HCAkKDw0NDhERCg0RHREKCA8VEg0RERoNDAwQGiYNDg8VIRUQNBMfISEYGRM0KhwjGhs0MioaIxwkIhgY', ); const PNG = b('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMYAQCABQAB6V282gAAAABJRU5ErkJggg=='); const WEBP = b('UklGRlQAAABXRUJQVlA4IBoAAAAwAQCdASoBAAEAAUAmJaQAA3AA/vuUAAA='); const AVIF = Buffer.concat([Buffer.alloc(4), Buffer.from('ftypavif', 'ascii')]); const GIF = b('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='); const HTML = Buffer.from('

Access Denied

The referenced entity does not exist

'); check('sniff: jpeg magic → image/jpeg', () => { assert.equal(sniffImage(JPEG), 'image/jpeg'); }); check('sniff: png magic → image/png', () => { assert.equal(sniffImage(PNG), 'image/png'); }); check('sniff: webp magic → image/webp', () => { assert.equal(sniffImage(WEBP), 'image/webp'); }); check('sniff: avif ftyp → image/avif', () => { assert.equal(sniffImage(AVIF), 'image/avif'); }); check('sniff: gif magic → image/gif', () => { assert.equal(sniffImage(GIF), 'image/gif'); }); check('sniff: HTML blockpage → null', () => { assert.equal(sniffImage(HTML), null); }); check('sniff: short/garbage buffer → null', () => { assert.equal(sniffImage(Buffer.from('ab')), null); }); check('ssrf: public https ok', () => { assert.ok(isSafeProxyTarget('https://ichef.bbci.co.uk/ace/branded_news/x.jpg')); }); check('ssrf: public http ok', () => { assert.ok(isSafeProxyTarget('http://example.com/a.webp')); }); check('ssrf: localhost blocked', () => { assert.ok(!isSafeProxyTarget('http://localhost:3000/images?url=x')); }); check('ssrf: 127/10/192.168/169.254 blocked', () => { assert.ok(!isSafeProxyTarget('http://127.0.0.1/a')); assert.ok(!isSafeProxyTarget('http://10.0.0.5/a')); assert.ok(!isSafeProxyTarget('http://192.168.1.2/a')); assert.ok(!isSafeProxyTarget('http://169.254.169.254/latest')); }); check('ssrf: 172.16-31 blocked, 172.15/172.32 ok', () => { assert.ok(!isSafeProxyTarget('http://172.17.0.1:11434/')); assert.ok(!isSafeProxyTarget('http://172.16.0.9/')); assert.ok(!isSafeProxyTarget('http://172.31.9.9/')); assert.ok(isSafeProxyTarget('http://172.15.255.1/')); // below private range assert.ok(isSafeProxyTarget('http://172.32.0.1/')); // above private range }); check('ssrf: ipv6 loopback/link-local/ULA blocked, mapped private blocked', () => { assert.ok(!isSafeProxyTarget('http://[::1]/a')); assert.ok(!isSafeProxyTarget('http://[fe80::1]/a')); assert.ok(!isSafeProxyTarget('http://[fd00::1]/a')); assert.ok(!isSafeProxyTarget('http://[::ffff:127.0.0.1]/a')); assert.ok(!isSafeProxyTarget('http://[::ffff:192.168.1.2]/a')); assert.ok(!isSafeProxyTarget('http://[::]/a')); }); check('ssrf: non-web schemes blocked', () => { assert.ok(!isSafeProxyTarget('file:///etc/passwd')); assert.ok(!isSafeProxyTarget('gopher://example.com')); assert.ok(!isSafeProxyTarget('blob:https://x.com/abc')); }); check('ssrf: our own origin rejected (no self-proxy)', () => { assert.ok(!isSafeProxyTarget('https://technews.krisforbes.ca/article/x')); }); check('ssrf: unparseable url → false', () => { assert.ok(!isSafeProxyTarget('not a url')); }); check('proxy: remote url → /images?url=...', () => { const u = 'https://data-api.investing.com/trkd-images/abc.jpg'; assert.equal(proxyImageUrl(u), `/images?url=${encodeURIComponent(u)}`); }); check('proxy: relative path passes through', () => { assert.equal(proxyImageUrl('/covers/tech.svg'), null); }); check('proxy: data URI passes through', () => { assert.equal(proxyImageUrl('data:image/png;base64,AAA='), null); }); check('proxy: own-origin url passes through', () => { assert.equal(proxyImageUrl('https://technews.krisforbes.ca/logo.png'), null); }); check('proxy: empty/blank → null', () => { assert.equal(proxyImageUrl(null), null); assert.equal(proxyImageUrl(' '), null); }); check('key: stable + 40 hex chars', () => { const k1 = mediaKey('https://x.com/a.jpg'); const k2 = mediaKey('https://x.com/a.jpg'); assert.equal(k1, k2); assert.match(k1, /^[0-9a-f]{40}$/); assert.notEqual(k1, mediaKey('https://x.com/b.jpg')); }); const total = passed + failed; console.log(`\n# tests ${total}, pass ${passed}, fail ${failed}`); process.exit(failed === 0 ? 0 : 1);