{"record":{"id":"36063d3318d62ce3","repo":"paperclipai/paperclip","slug":"animation-must-contain-only-visual-html-css-or-inline-svg","errorCode":null,"errorMessage":"Animation must contain only visual HTML/CSS or inline SVG; scripts, navigation, resources and interactive elements are not supported","messagePattern":"Animation must contain only visual HTML/CSS or inline SVG; scripts, navigation, resources and interactive elements are not supported","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/announcement-animation.ts","lineNumber":27,"sourceCode":"  if (!bytes.length || bytes.byteLength > ANNOUNCEMENT_ANIMATION_MAX_BYTES) {\n    throw new Error(\"Invalid or oversized announcement animation\");\n  }\n  const source = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n  const dom = new JSDOM(\"\");\n  try {\n    const purifier = createDOMPurify(dom.window as unknown as Parameters<typeof createDOMPurify>[0]);\n    const html = purifier.sanitize(source, {\n      WHOLE_DOCUMENT: true,\n      ALLOWED_TAGS: [\"html\", \"head\", \"body\", \"style\", \"div\", \"span\", \"p\", \"br\", \"strong\", \"em\", \"b\", \"i\",\n        \"svg\", \"g\", \"path\", \"circle\", \"ellipse\", \"rect\", \"line\", \"polyline\", \"polygon\", \"text\", \"tspan\", \"title\", \"desc\"],\n      ALLOWED_ATTR: [\"class\", \"id\", \"style\", \"viewBox\", \"xmlns\", \"width\", \"height\", \"x\", \"y\", \"x1\", \"x2\", \"y1\", \"y2\",\n        \"cx\", \"cy\", \"r\", \"rx\", \"ry\", \"d\", \"points\", \"fill\", \"stroke\", \"stroke-width\", \"stroke-linecap\",\n        \"stroke-linejoin\", \"stroke-dasharray\", \"stroke-dashoffset\", \"opacity\", \"transform\", \"text-anchor\"],\n      ALLOW_DATA_ATTR: false,\n      ALLOW_ARIA_ATTR: false,\n    });\n    if (purifier.removed.length) {\n      throw new Error(\"Animation must contain only visual HTML/CSS or inline SVG; scripts, navigation, resources and interactive elements are not supported\");\n    }\n    return html;\n  } finally {\n    dom.window.close();\n  }\n}\n","sourceCodeStart":9,"sourceCodeEnd":34,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/announcement-animation.ts#L9-L34","documentation":"After DOMPurify sanitizes the animation document with a strict allowlist (visual HTML/CSS and inline SVG only, no data/aria attributes), any element or attribute DOMPurify had to remove causes this error. The animation is treated as a purely visual document — scripts, navigation, remote resources, and interactive elements are categorically rejected rather than silently stripped, so publishers know their content violates the policy.","triggerScenarios":"Uploading an animation containing script tags, event handlers, form/input elements, links with navigation, url(...) resource loads, iframes, or any attribute outside the ALLOWED_TAGS/ALLOWED_ATTR allowlist that DOMPurify removes during sanitization.","commonSituations":"Exporting from an animation tool that injects script loaders or data-URI attributes; including <a href> click-throughs or <button> elements in a promo animation; CSS referencing external fonts/images; hand-authored SVG with event attributes like onload.","solutions":["Remove all scripts, event-handler attributes, forms, links, iframes, and external resource references from the animation.","Restrict markup to the visual allowlist: basic HTML/CSS and inline SVG with presentational attributes only (fill, stroke, transform, opacity, etc.).","Move any interactivity out of the animation; announcements are static visual documents.","Test locally by running the same DOMPurify config and checking purifier.removed is empty before publishing."],"exampleFix":"// before\n<div class=\"anim\" onclick=\"play()\"><script src=\"tracker.js\"></script><a href=\"https://x\">Go</a></div>\n// after\n<div class=\"anim\"><svg viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"40\" fill=\"currentColor\"/></svg></div>","handlingStrategy":"validation","validationCode":"import { JSDOM } from \"jsdom\"; import createDOMPurify from \"dompurify\";\nconst dom = new JSDOM(\"\");\nconst purifier = createDOMPurify(dom.window);\npurifier.sanitize(source, { ALLOWED_TAGS, ALLOWED_ATTR, ALLOW_DATA_ATTR: false, ALLOW_ARIA_ATTR: false });\nif (purifier.removed.length) throw new Error(`Forbidden content: ${purifier.removed.map(r => r.element ?? r.attribute)}`);\ndom.window.close();","typeGuard":"null","tryCatchPattern":"try { await publishAnnouncement({ kind: \"animation\", bytes }); }\ncatch (e) {\n  if (e.message.startsWith(\"Animation must contain only visual\")) {\n    return stripForbiddenMarkup(assetPath); // remove scripts/links/forms, retry\n  }\n  throw e;\n}","preventionTips":["Lint animations for script/a/button/iframe tags before publishing","Avoid event-handler attributes and url() external references in CSS","Run the same DOMPurify allowlist locally before upload","Keep animations purely presentational; move interactivity elsewhere"],"tags":["sanitization","xss-prevention","announcements"],"backgroundTag":"schema-validation-failed","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}