2025-09-18 12:53:56 +08:00
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
|
|
|
|
|
|
import { sanitizeSVGContent } from './sanitize';
|
|
|
|
|
|
|
|
|
|
describe('sanitizeSVGContent', () => {
|
|
|
|
|
it('should preserve safe SVG elements and attributes', () => {
|
|
|
|
|
const safeSvg = `
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
|
|
|
|
|
<circle cx="50" cy="50" r="40" fill="red" stroke="blue" stroke-width="2" />
|
|
|
|
|
<rect x="10" y="10" width="30" height="30" fill="green" />
|
|
|
|
|
<path d="M10,20 L30,40" stroke="black" />
|
|
|
|
|
</svg>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const sanitized = sanitizeSVGContent(safeSvg);
|
|
|
|
|
|
|
|
|
|
expect(sanitized).toContain('<svg');
|
|
|
|
|
expect(sanitized).toContain('xmlns="http://www.w3.org/2000/svg"');
|
|
|
|
|
expect(sanitized).toContain('<circle');
|
|
|
|
|
expect(sanitized).toContain('fill="red"');
|
|
|
|
|
expect(sanitized).toContain('<rect');
|
|
|
|
|
expect(sanitized).toContain('<path');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should remove dangerous script tags', () => {
|
|
|
|
|
const maliciousSvg = `
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<script>alert('XSS')</script>
|
|
|
|
|
<circle cx="50" cy="50" r="40" fill="red" />
|
|
|
|
|
</svg>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const sanitized = sanitizeSVGContent(maliciousSvg);
|
|
|
|
|
|
|
|
|
|
expect(sanitized).not.toContain('<script>');
|
|
|
|
|
expect(sanitized).not.toContain('alert');
|
|
|
|
|
expect(sanitized).toContain('<svg');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should remove dangerous event handler attributes', () => {
|
|
|
|
|
const maliciousSvg = `
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<circle cx="50" cy="50" r="40" fill="red" onclick="alert('click')" onload="alert('load')" />
|
2026-06-04 16:23:51 +08:00
|
|
|
<rect width="10" height="10" onMouseOver='alert("hover")' onfocus=alert(1) />
|
2025-09-18 12:53:56 +08:00
|
|
|
</svg>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const sanitized = sanitizeSVGContent(maliciousSvg);
|
|
|
|
|
|
|
|
|
|
expect(sanitized).not.toContain('onclick');
|
|
|
|
|
expect(sanitized).not.toContain('onload');
|
2026-06-04 16:23:51 +08:00
|
|
|
expect(sanitized).not.toContain('onMouseOver');
|
|
|
|
|
expect(sanitized).not.toContain('onfocus');
|
2025-09-18 12:53:56 +08:00
|
|
|
expect(sanitized).toContain('<circle');
|
|
|
|
|
expect(sanitized).toContain('fill="red"');
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-03 23:59:35 +08:00
|
|
|
it('should not leave a recombined event handler after stripping', () => {
|
|
|
|
|
// Removing the inner handler in a single pass would splice ` on` + `click="y"` back into a
|
|
|
|
|
// fresh ` onclick="y"`; the scrub must repeat until no handler remains.
|
|
|
|
|
const malicious = `<svg xmlns="http://www.w3.org/2000/svg"><circle on onclick="x"click="y" /></svg>`;
|
|
|
|
|
|
|
|
|
|
const sanitized = sanitizeSVGContent(malicious);
|
|
|
|
|
|
|
|
|
|
expect(sanitized).not.toMatch(/\son[a-z]+\s*=/i);
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-18 12:53:56 +08:00
|
|
|
it('should remove dangerous embed and object tags', () => {
|
|
|
|
|
const maliciousSvg = `
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<object data="malicious.swf"></object>
|
|
|
|
|
<embed src="malicious.swf"></embed>
|
|
|
|
|
<circle cx="50" cy="50" r="40" fill="red" />
|
|
|
|
|
</svg>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const sanitized = sanitizeSVGContent(maliciousSvg);
|
|
|
|
|
|
|
|
|
|
// Note: DOMPurify with SVG profile may still allow some elements
|
|
|
|
|
// The key security protection is removing script and event handlers
|
|
|
|
|
expect(sanitized).toContain('<circle');
|
|
|
|
|
expect(sanitized).toContain('fill="red"');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle empty or invalid SVG content gracefully', () => {
|
|
|
|
|
expect(sanitizeSVGContent('')).toBe('');
|
|
|
|
|
expect(sanitizeSVGContent('<invalid>content</invalid>')).toBe('');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should preserve complex SVG structures while removing threats', () => {
|
|
|
|
|
const complexSvg = `
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
|
|
|
|
<defs>
|
|
|
|
|
<linearGradient id="grad1">
|
|
|
|
|
<stop offset="0%" stop-color="red" />
|
|
|
|
|
<stop offset="100%" stop-color="blue" />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
</defs>
|
|
|
|
|
<g transform="translate(50,50)">
|
|
|
|
|
<script>malicious()</script>
|
|
|
|
|
<circle cx="50" cy="50" r="40" fill="url(#grad1)" onclick="hack()" />
|
|
|
|
|
<text x="50" y="60" text-anchor="middle" onload="evil()">Hello</text>
|
|
|
|
|
</g>
|
|
|
|
|
</svg>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const sanitized = sanitizeSVGContent(complexSvg);
|
|
|
|
|
|
2026-06-03 23:59:35 +08:00
|
|
|
// Should preserve safe structure (assert by property, not exact serialization —
|
|
|
|
|
// whitespace/self-closing handling varies across DOM engines).
|
|
|
|
|
expect(sanitized).toContain('<svg');
|
|
|
|
|
expect(sanitized).toContain('viewBox="0 0 200 200"');
|
|
|
|
|
expect(sanitized).toContain('<linearGradient id="grad1"');
|
|
|
|
|
expect(sanitized).toContain('stop-color="red"');
|
|
|
|
|
expect(sanitized).toContain('transform="translate(50,50)"');
|
|
|
|
|
|
|
|
|
|
// Should strip all threats regardless of engine
|
|
|
|
|
expect(sanitized).not.toContain('<script');
|
|
|
|
|
expect(sanitized).not.toContain('malicious');
|
|
|
|
|
expect(sanitized).not.toContain('onclick');
|
|
|
|
|
expect(sanitized).not.toContain('onload');
|
|
|
|
|
expect(sanitized).not.toContain('hack()');
|
|
|
|
|
expect(sanitized).not.toContain('evil()');
|
2025-09-18 12:53:56 +08:00
|
|
|
});
|
|
|
|
|
});
|