/** @jest-environment node */ import { assertProxyMediaFetchTargetIsSafe, createProxyMediaPolicy, isPublicRoutableIp, parseMediaProxyTarget, ProxyMediaSecurityError, resolveInitialProxyMediaFetchTarget, resolveProxyMediaRedirect, } from '@/lib/proxy-media-security' const policy = createProxyMediaPolicy({ S3_PUBLIC_DOMAIN: 'https://s3.jiansun.vip/gongxiangpan', S3_ENDPOINT: 'http://192.168.58.20:9000', SERVER_IP: undefined, MINIO_API_PORT: undefined, }) describe('proxy media security policy', () => { test('只允许明确的 S3 域名、媒体扩展名和端口', () => { expect(parseMediaProxyTarget('https://s3.1080p.hk/assets/a.jpg', policy).hostname) .toBe('s3.1080p.hk') expect(() => parseMediaProxyTarget('http://localhost:9000/a.jpg', policy)) .toThrow(ProxyMediaSecurityError) expect(() => parseMediaProxyTarget('https://sub.s3.jiansun.vip/a.jpg', policy)) .toThrow(ProxyMediaSecurityError) expect(() => parseMediaProxyTarget('https://s3.jiansun.vip:444/a.jpg', policy)) .toThrow('不允许的媒体端口') expect(() => parseMediaProxyTarget('https://user:pass@s3.jiansun.vip/a.jpg', policy)) .toThrow('不能包含账号或密码') expect(() => parseMediaProxyTarget('https://s3.jiansun.vip/a.html', policy)) .toThrow('不允许的媒体文件类型') }) test('兼容历史双重编码 URL,但仍执行同一安全校验', () => { const encoded = encodeURIComponent('https://s3.1080p.hk/assets/a.webp') expect(parseMediaProxyTarget(encoded, policy).pathname).toBe('/assets/a.webp') }) test.each([ '0.0.0.0', '10.0.0.1', '100.64.0.1', '127.0.0.1', '169.254.169.254', '172.16.0.1', '192.168.58.20', '198.18.0.1', '::1', 'fc00::1', 'fe80::1', '2001:db8::1', ])('拒绝不可公网路由地址 %s', (address) => { expect(isPublicRoutableIp(address)).toBe(false) }) test.each(['1.1.1.1', '8.8.8.8', '2606:4700:4700::1111'])( '允许公网路由地址 %s', (address) => { expect(isPublicRoutableIp(address)).toBe(true) }, ) test('DNS 只要返回任意内网地址就拒绝,防止多记录绕过', async () => { const target = resolveInitialProxyMediaFetchTarget( parseMediaProxyTarget('https://s3.1080p.hk/a.jpg', policy), policy, ) await expect(assertProxyMediaFetchTargetIsSafe( target, async () => ['1.1.1.1', '127.0.0.1'], )).rejects.toThrow('受保护的内网地址') }) test('仅当全部 DNS 结果为公网地址时才允许公网回源', async () => { const target = resolveInitialProxyMediaFetchTarget( parseMediaProxyTarget('https://s3.1080p.hk/a.jpg', policy), policy, ) await expect(assertProxyMediaFetchTargetIsSafe( target, async () => ['1.1.1.1', '2606:4700:4700::1111'], )).resolves.toBeUndefined() }) test('配置中的 S3 公网域名只会回源到精确的受信任内网 S3', async () => { const target = resolveInitialProxyMediaFetchTarget( parseMediaProxyTarget('https://s3.jiansun.vip/gongxiangpan/a.jpg?token=secret', policy), policy, ) expect(target.trustedInternalS3).toBe(true) expect(target.url.origin).toBe('http://192.168.58.20:9000') expect(target.url.pathname).toBe('/gongxiangpan/a.jpg') expect(target.url.searchParams.get('token')).toBe('secret') const resolver = jest.fn(async () => ['192.168.58.20']) await expect(assertProxyMediaFetchTargetIsSafe(target, resolver)).resolves.toBeUndefined() expect(resolver).not.toHaveBeenCalled() }) test('内网 S3 跳转只能留在同一个受信任源站', () => { const target = resolveInitialProxyMediaFetchTarget( parseMediaProxyTarget('https://s3.jiansun.vip/gongxiangpan/a.jpg', policy), policy, ) expect(resolveProxyMediaRedirect(target, '/gongxiangpan/b.jpg', policy).trustedInternalS3) .toBe(true) expect(() => resolveProxyMediaRedirect(target, 'http://169.254.169.254/latest/meta-data', policy)) .toThrow('不允许跳转到其它地址') }) test('公网回源的每次跳转都重新限制域名和端口', () => { const target = resolveInitialProxyMediaFetchTarget( parseMediaProxyTarget('https://s3.1080p.hk/a.jpg', policy), policy, ) expect(() => resolveProxyMediaRedirect(target, 'http://127.0.0.1/a.jpg', policy)) .toThrow('不允许的媒体域名') expect(() => resolveProxyMediaRedirect(target, 'https://s3.1080p.hk:8443/a.jpg', policy)) .toThrow('不允许的媒体端口') }) })