gongxiangp2/lib/__tests__/proxy-media-security.test.ts

130 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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('不允许的媒体端口')
})
})