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