feat(icons): SVG sanitizer for uploaded icons
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
16
lib/icons/sanitize.js
Normal file
16
lib/icons/sanitize.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// lib/icons/sanitize.js
|
||||||
|
// Focused SVG sanitizer for owner-uploaded icons. NOT a general-purpose
|
||||||
|
// sanitizer — it removes the script/handler/foreignObject/js-uri vectors that
|
||||||
|
// matter for inline-rendered icons. (Owner-only upload behind CF Access.)
|
||||||
|
export function sanitizeSvg(input) {
|
||||||
|
let s = Buffer.isBuffer(input) ? input.toString('utf8') : String(input);
|
||||||
|
s = s.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||||
|
s = s.replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, '');
|
||||||
|
s = s.replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, '');
|
||||||
|
s = s.replace(/\son[a-z]+\s*=\s*'[^']*'/gi, '');
|
||||||
|
// Unquoted handlers, e.g. <svg onload=alert(1)>. Value runs until whitespace,
|
||||||
|
// quote, or the tag's closing > / />.
|
||||||
|
s = s.replace(/\son[a-z]+\s*=\s*[^\s">]+/gi, '');
|
||||||
|
s = s.replace(/(href|xlink:href)\s*=\s*("|')\s*javascript:[^"']*\2/gi, '$1=$2#$2');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
29
tests/icons/sanitize.test.js
Normal file
29
tests/icons/sanitize.test.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { sanitizeSvg } from '../../lib/icons/sanitize.js';
|
||||||
|
|
||||||
|
describe('sanitizeSvg', () => {
|
||||||
|
it('strips <script> tags', () => {
|
||||||
|
const out = sanitizeSvg('<svg><script>alert(1)</script><path d="M0 0"/></svg>');
|
||||||
|
expect(out).not.toMatch(/script/i);
|
||||||
|
expect(out).toMatch(/<path/);
|
||||||
|
});
|
||||||
|
it('strips on* event handlers', () => {
|
||||||
|
const out = sanitizeSvg('<svg onload="x()"><rect onclick="y()"/></svg>');
|
||||||
|
expect(out).not.toMatch(/onload|onclick/i);
|
||||||
|
});
|
||||||
|
it('strips unquoted on* handlers', () => {
|
||||||
|
const out = sanitizeSvg('<svg onload=alert(1)><rect onclick=go()/></svg>');
|
||||||
|
expect(out).not.toMatch(/onload|onclick/i);
|
||||||
|
});
|
||||||
|
it('neutralizes javascript: hrefs', () => {
|
||||||
|
const out = sanitizeSvg('<svg><a href="javascript:alert(1)">x</a></svg>');
|
||||||
|
expect(out).not.toMatch(/javascript:/i);
|
||||||
|
});
|
||||||
|
it('drops <foreignObject>', () => {
|
||||||
|
const out = sanitizeSvg('<svg><foreignObject><body>x</body></foreignObject></svg>');
|
||||||
|
expect(out).not.toMatch(/foreignObject/i);
|
||||||
|
});
|
||||||
|
it('accepts a Buffer', () => {
|
||||||
|
expect(sanitizeSvg(Buffer.from('<svg><path/></svg>'))).toMatch(/<svg/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user