feat: harden SSRF protection with comprehensive security improvements
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 10m4s

- Block IPv4-mapped IPv6 addresses (::ffff:127.0.0.1, etc.)
- Block IPv6 unspecified address (::)
- Add CSS injection sanitization for hideSelectors (no {}<>;)
- Add waitForSelector validation (max 200 chars, no javascript:/script)
- Add CSS parameter hardening (block @import, url() with non-data: schemes)
- Add 21 new security tests following TDD approach
- All 387 tests passing

Fixes potential SSRF bypasses and CSS injection vulnerabilities
This commit is contained in:
SnapAPI Security Hardening 2026-03-05 09:04:59 +01:00
parent 0999474fbd
commit ba888bb580
4 changed files with 236 additions and 0 deletions

View file

@ -282,6 +282,116 @@ describe('Screenshot Service', () => {
})
})
describe('CSS Injection Prevention', () => {
describe('hideSelectors sanitization', () => {
it('should reject hideSelectors containing curly braces', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
hideSelectors: ['.safe', 'body { background: red; }', '.another']
})).rejects.toThrow('hideSelector contains dangerous characters')
})
it('should reject hideSelectors containing angle brackets', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
hideSelectors: ['<script>alert(1)</script>']
})).rejects.toThrow('hideSelector contains dangerous characters')
})
it('should reject hideSelectors containing semicolon', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
hideSelectors: ['body; background: red']
})).rejects.toThrow('hideSelector contains dangerous characters')
})
it('should accept safe hideSelectors', async () => {
await takeScreenshot({
url: 'https://example.com',
hideSelectors: ['.ad', '#banner', 'div.popup', 'nav ul li']
})
expect(mockPage.addStyleTag).toHaveBeenCalledWith({
content: '.ad { display: none !important }\n#banner { display: none !important }\ndiv.popup { display: none !important }\nnav ul li { display: none !important }'
})
})
})
describe('waitForSelector sanitization', () => {
it('should reject waitForSelector longer than 200 characters', async () => {
const longSelector = 'div'.repeat(100) // 300 chars
await expect(takeScreenshot({
url: 'https://example.com',
waitForSelector: longSelector
})).rejects.toThrow('waitForSelector is too long')
})
it('should reject waitForSelector containing javascript:', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
waitForSelector: 'javascript:alert(1)'
})).rejects.toThrow('waitForSelector contains dangerous content')
})
it('should reject waitForSelector containing script tag', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
waitForSelector: '<script>alert(1)</script>'
})).rejects.toThrow('waitForSelector contains dangerous content')
})
it('should accept safe waitForSelector', async () => {
await takeScreenshot({
url: 'https://example.com',
waitForSelector: '#main-content'
})
expect(mockPage.waitForSelector).toHaveBeenCalledWith('#main-content', { timeout: 10_000 })
})
})
describe('CSS parameter hardening', () => {
it('should reject CSS containing @import', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
css: 'body { color: red; } @import url(http://evil.com/steal.css);'
})).rejects.toThrow('CSS contains dangerous directives')
})
it('should reject CSS containing url() with http scheme', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
css: 'body { background: url(http://evil.com/image.png); }'
})).rejects.toThrow('CSS contains dangerous directives')
})
it('should reject CSS containing url() with https scheme', async () => {
await expect(takeScreenshot({
url: 'https://example.com',
css: 'body { background: url(https://evil.com/image.png); }'
})).rejects.toThrow('CSS contains dangerous directives')
})
it('should allow CSS with data: URLs', async () => {
await takeScreenshot({
url: 'https://example.com',
css: 'body { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==); }'
})
expect(mockPage.addStyleTag).toHaveBeenCalledWith({
content: 'body { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==); }'
})
})
it('should allow safe CSS without external references', async () => {
await takeScreenshot({
url: 'https://example.com',
css: 'body { color: red; margin: 0; padding: 10px; }'
})
expect(mockPage.addStyleTag).toHaveBeenCalledWith({
content: 'body { color: red; margin: 0; padding: 10px; }'
})
})
})
})
describe('Page lifecycle', () => {
it('always releases page after successful screenshot', async () => {
await takeScreenshot({ url: 'https://example.com' })