feat: harden SSRF protection with comprehensive security improvements
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 10m4s
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:
parent
0999474fbd
commit
ba888bb580
4 changed files with 236 additions and 0 deletions
|
|
@ -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' })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue