You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

73 line
2.8 KiB

  1. // Sanitizer SVG focalizzato sul caso "logo del portale caricato dall'admin".
  2. // Approccio: rimozione regex-based dei costrutti pericolosi (deny-list aggressiva +
  3. // validazione che il contenuto sia davvero SVG). Non è una difesa esaustiva da
  4. // payload SVG sofisticati: per quello servirebbe DOMPurify+jsdom o xmldom.
  5. // Per il nostro modello di minaccia (admin trusted carica il proprio logo) basta.
  6. // Defense-in-depth: il file servito da /api/files include CSP `script-src 'none'`
  7. // quando il MIME è image/svg+xml.
  8. // Tag che vanno completamente rimossi (anche il contenuto):
  9. // - script/foreignObject/iframe/embed/object: codice eseguibile
  10. // - style/link/meta: possono caricare risorse esterne o iniettare CSS dannoso
  11. // - set/animate*: animation handlers possono triggerare eventi
  12. const FORBIDDEN_TAGS = [
  13. 'script',
  14. 'foreignObject',
  15. 'iframe',
  16. 'embed',
  17. 'object',
  18. 'link',
  19. 'meta',
  20. 'style',
  21. 'set',
  22. 'animate',
  23. 'animateMotion',
  24. 'animateTransform',
  25. ] as const;
  26. const EVENT_ATTR_RE = /\s+on\w+\s*=\s*("[^"]*"|'[^']*'|\S+)/gi;
  27. // href/xlink:href con schema non consentito.
  28. // Sono ammessi: #fragment, http(s)://, mailto:, tel:. Tutto il resto via.
  29. const UNSAFE_HREF_RE = /\s+(xlink:)?href\s*=\s*("|')\s*(?!#|https?:|mailto:|tel:|\2)[^"']*\2/gi;
  30. // Rimuove strighe `javascript:` o `data:text/html` anche dentro attributi che non
  31. // passano dal pattern href (es. attributeName, values, ecc.) — best effort.
  32. const SCHEME_INLINE_RE = /(javascript:|data:text\/html)/gi;
  33. export type SvgSanitizeResult =
  34. | { ok: true; sanitized: string }
  35. | { ok: false; error: string };
  36. export function sanitizeSvg(input: string): SvgSanitizeResult {
  37. if (typeof input !== 'string' || input.length === 0) {
  38. return { ok: false, error: 'The SVG file is empty. Choose a non-empty .svg file.' };
  39. }
  40. // Deve essere un documento SVG: opzionalmente un XML declaration, opzionalmente
  41. // commenti, e poi un tag <svg>.
  42. if (!/^\s*(<\?xml[^?]*\?>\s*)?(<!--[\s\S]*?-->\s*)*<svg[\s>]/i.test(input)) {
  43. return { ok: false, error: 'Not a valid SVG document: the file does not start with an <svg> tag. It may be corrupted or be a different format saved with an .svg extension.' };
  44. }
  45. let out = input;
  46. // 1) Rimuove i tag vietati (sia open+close che self-closing)
  47. for (const tag of FORBIDDEN_TAGS) {
  48. // <tag ...>...</tag>
  49. out = out.replace(new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?</${tag}>`, 'gi'), '');
  50. // <tag ... /> oppure <tag ...>
  51. out = out.replace(new RegExp(`<${tag}\\b[^>]*/?>`, 'gi'), '');
  52. }
  53. // 2) Rimuove event handler (onload, onclick, onerror, ecc.)
  54. out = out.replace(EVENT_ATTR_RE, '');
  55. // 3) Strippa href/xlink:href con schema non ammesso
  56. out = out.replace(UNSAFE_HREF_RE, '');
  57. // 4) Rimozione difensiva di `javascript:` / `data:text/html` ovunque appaiano
  58. out = out.replace(SCHEME_INLINE_RE, '');
  59. return { ok: true, sanitized: out };
  60. }