fix(product-archive): allow trusted HTTPS image host on port 3333

This commit is contained in:
WuYongJie 2026-10-14 18:14:47 +08:00
parent e57f95c489
commit b6a326d996
2 changed files with 68 additions and 2 deletions

View File

@ -71,6 +71,66 @@ describe('product archive source image service', () => {
expect(fetchImpl).toHaveBeenCalledTimes(2)
})
it('allows the company HTTPS image host on port 3333', async () => {
const imageUrl = 'https://static.jiansun.vip:3333/Resource/SkuImg/YMJ000904N754V01.jpg'
const fetchImpl = jest.fn().mockResolvedValue(
response({ contentType: 'image/jpeg' }),
) as unknown as typeof fetch
const service = new ProductArchiveSourceImageService(
repository(imageUrl),
fetchImpl,
jest.fn().mockResolvedValue(['93.184.216.34']),
)
await expect(service.readByBarcode('YMJ000904N722')).resolves.toEqual(
expect.objectContaining({ contentType: 'image/jpeg' }),
)
expect(fetchImpl).toHaveBeenCalledWith(new URL(imageUrl), expect.anything())
})
it.each([
'https://images.example.com:3333/product.jpg',
'https://static.jiansun.vip.example.com:3333/product.jpg',
'https://static.jiansun.vip:8443/product.jpg',
'http://static.jiansun.vip:3333/product.jpg',
])('keeps untrusted image ports blocked: %s', async (imageUrl) => {
const fetchImpl = jest.fn() as unknown as typeof fetch
const service = new ProductArchiveSourceImageService(
repository(imageUrl),
fetchImpl,
jest.fn().mockResolvedValue(['93.184.216.34']),
)
await expect(service.readByBarcode('BARCODE-1')).rejects.toThrow('port is not allowed')
expect(fetchImpl).not.toHaveBeenCalled()
})
it('keeps private addresses blocked for the trusted image host', async () => {
const fetchImpl = jest.fn() as unknown as typeof fetch
const service = new ProductArchiveSourceImageService(
repository('https://static.jiansun.vip:3333/product.jpg'),
fetchImpl,
jest.fn().mockResolvedValue(['127.0.0.1']),
)
await expect(service.readByBarcode('BARCODE-1')).rejects.toThrow('private address')
expect(fetchImpl).not.toHaveBeenCalled()
})
it('validates redirects from the trusted host before following them', async () => {
const fetchImpl = jest.fn().mockResolvedValue(
response({ status: 302, location: 'https://images.example.com:3333/product.jpg' }),
) as unknown as typeof fetch
const service = new ProductArchiveSourceImageService(
repository('https://static.jiansun.vip:3333/product.jpg'),
fetchImpl,
jest.fn().mockResolvedValue(['93.184.216.34']),
)
await expect(service.readByBarcode('BARCODE-1')).rejects.toThrow('port is not allowed')
expect(fetchImpl).toHaveBeenCalledTimes(1)
})
it('blocks source URLs that resolve to a private address', async () => {
const sourceRepository = repository('http://internal.example.com/product.jpg')
const fetchImpl = jest.fn() as unknown as typeof fetch

View File

@ -83,9 +83,15 @@ function isPrivateAddress(address: string) {
async function assertPublicImageUrl(url: URL, resolveAddresses: ResolveAddresses) {
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Unsupported image protocol')
if (url.username || url.password) throw new Error('Image URL credentials are not allowed')
if (url.port && !['80', '443'].includes(url.port)) throw new Error('Image URL port is not allowed')
const hostname = url.hostname.trim().toLowerCase()
// JST SKU photos also use the company's HTTPS image host on port 3333.
const isTrustedImagePort = url.protocol === 'https:'
&& hostname === 'static.jiansun.vip'
&& url.port === '3333'
if (url.port && !['80', '443'].includes(url.port) && !isTrustedImagePort) {
throw new Error('Image URL port is not allowed')
}
if (!hostname || hostname === 'localhost' || hostname.endsWith('.local')) {
throw new Error('Image URL hostname is not public')
}