{"record":{"id":"9d553b423a044830","repo":"basecamp/trix","slug":"root-node-is-forbidden-and-cannot-be-sanitized-in","errorCode":null,"errorMessage":"root node is forbidden and cannot be sanitized in-place","messagePattern":"root node is forbidden and cannot be sanitized in-place","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"action_text-trix/app/assets/javascripts/trix.js","lineNumber":4019,"sourceCode":"      if (inPlace) {\n        /* Declarative-partial-updates / streaming pre-pass: sever every patch\n           linkage across the live tree BEFORE the walk, so no patch can fire\n           mid-walk and inject into an already-processed region. Runs first, so\n           it also covers the forbidden/clobbered roots that throw below. */\n        _neutralizePatchLinkage(dirty);\n        /* Do some early pre-sanitization to avoid unsafe root nodes.\n           Read nodeName through the cached prototype getter — a clobbering\n           child named \"nodeName\" on the form root would otherwise shadow\n           the property and let this check skip the root-allowlist\n           validation entirely. */\n        const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;\n        if (typeof nn === 'string') {\n          const tagName = transformCaseFunc(nn);\n          if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n            /* Fail closed on a live root: neutralize handlers/children before\n               throwing, exactly as the mid-walk abort path does. */\n            _neutralizeRoot(dirty);\n            throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');\n          }\n        }\n        /* Pre-flight the root through _isClobbered. The iterator-driven\n           removal path can not detach a parent-less root: _forceRemove\n           falls through to Element.prototype.remove(), which per spec\n           is a no-op on a node with no parent. A clobbered root would\n           then survive the main loop with its attributes uninspected,\n           because _sanitizeAttributes early-returns on _isClobbered. The\n           result would be an attacker-controlled form, complete with any\n           event-handler attributes the caller passed in, handed back to\n           the application unsanitized. Refuse to sanitize such a root\n           the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */\n        if (_isClobbered(dirty)) {\n          /* Fail closed on a live clobbered root before throwing.\n             _neutralizeRoot's reads are clobber-safe (cached getters); the\n             form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */\n          _neutralizeRoot(dirty);\n          throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');","sourceCodeStart":4001,"sourceCodeEnd":4037,"githubUrl":"https://github.com/basecamp/trix/blob/470040131122bd44e269b4de0f2e9557f90ec994/action_text-trix/app/assets/javascripts/trix.js#L4001-L4037","documentation":"In in-place sanitization (sanitizing a live DOM root), DOMPurify pre-flights the root node itself: if the root's tagName is not in ALLOWED_TAGS (or is in FORBID_TAGS), the entire root is forbidden. Because the caller requested in-place sanitization, DOMPurify cannot substitute a safe replacement, so it first neutralizes the root (_neutralizeRoot: scrubbing handlers/children fail-closed) and then throws this TypeError instead of returning the unsafe tree.","triggerScenarios":"DOMPurify.sanitize(rootElement, { IN_PLACE: true, ... }) where the root element's tag is disallowed by the current tag allowlist/forbidlist — e.g. sanitizing a <script>, <iframe> or custom element as the root while it is not in ALLOWED_TAGS/ADD_TAGS (or is explicitly FORBID_TAGS).","commonSituations":"Sanitizing a whole <form> or <body>-adjacent element whose tag was later FORBID_TAGS'd; tightening the default allowlist (e.g. USE_PROFILES) so the root element itself is no longer allowed; sanitizing custom elements before adding them via ADD_TAGS.","solutions":["Allow the root tag: add it via ALLOWED_TAGS/ADD_TAGS (or adjust USE_PROFILES) if the root element is genuinely safe to keep.","Alternatively sanitize an inner container that has an allowed tag, or serialize to HTML and sanitize the string instead of in-place mode.","If the root should never be allowed, replace it before sanitizing (e.g. wrap children in a <div> root).","Catch the TypeError and fail closed: discard the node or rebuild it from sanitized outerHTML of its children."],"exampleFix":"// before\nDOMPurify.sanitize(customEl, { IN_PLACE: true, ADD_TAGS: [] }); // throws: root forbidden\n// after\nDOMPurify.sanitize(customEl, { IN_PLACE: true, ADD_TAGS: ['my-widget'] });\n// or sanitize an allowed wrapper instead\nDOMPurify.sanitize(wrapperDiv, { IN_PLACE: true });","handlingStrategy":"validation","validationCode":"const tag = root.tagName.toLowerCase();\nconst cfg = { ALLOWED_TAGS: DOMPurify.defaults.ALLOWED_TAGS /* + your additions */ };\nif (!cfg.ALLOWED_TAGS.includes(tag)) {\n  // swap root for an allowed wrapper or add the tag via ADD_TAGS before calling\n}","typeGuard":"const isAllowedRoot = (n, allowedTags) => n instanceof Element && allowedTags.includes(n.tagName.toLowerCase());","tryCatchPattern":"try {\n  DOMPurify.sanitize(root, { IN_PLACE: true, ADD_TAGS: ['my-widget'] });\n} catch (e) {\n  if (String(e.message).includes('root node is forbidden')) {\n    const safe = DOMPurify.sanitize(root.outerHTML); // rebuild from string\n  } else { throw e; }\n}","preventionTips":["Keep the root element's tag in ALLOWED_TAGS/ADD_TAGS whenever using IN_PLACE.","Re-check the allowlist after tightening USE_PROFILES or adding FORBID_TAGS.","Sanitize an inner allowed wrapper instead of exotic root elements.","Prefer string sanitization when the root tag may vary."],"tags":["dompurify","dom-sanitization","in-place","allowed-tags"],"backgroundTag":"unsanitizable-dom-node","analyzedSha":"470040131122bd44e269b4de0f2e9557f90ec994","analyzedAt":"2026-09-02T10:19:15.878Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T16:17:10.729Z"}