{"record":{"id":"f41606e238f74e05","repo":"basecamp/trix","slug":"root-node-is-clobbered-and-cannot-be-sanitized-in","errorCode":null,"errorMessage":"root node is clobbered and cannot be sanitized in-place","messagePattern":"root node is clobbered and cannot be sanitized in-place","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"action_text-trix/app/assets/javascripts/trix.js","lineNumber":4037,"sourceCode":"            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');\n        }\n        /* Sanitize attached shadow roots before the main iterator runs.\n           The iterator does not descend into shadow trees. Same fail-closed\n           barrier as the main walk (campaign-3 F2): a custom-element reaction\n           inside a shadow root could abort this pre-pass before the walk runs,\n           which would otherwise leave the entire live tree unsanitized. */\n        try {\n          _sanitizeAttachedShadowRoots(dirty);\n        } catch (error) {\n          _neutralizeRoot(dirty);\n          throw error;\n        }\n      } else if (_isNode(dirty)) {\n        /* If dirty is a DOM element, append to an empty document to avoid\n           elements being stripped by the parser */\n        body = _initDocument('<!---->');\n        importedNode = body.ownerDocument.importNode(dirty, true);\n        if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {","sourceCodeStart":4019,"sourceCodeEnd":4055,"githubUrl":"https://github.com/basecamp/trix/blob/470040131122bd44e269b4de0f2e9557f90ec994/action_text-trix/app/assets/javascripts/trix.js#L4019-L4055","documentation":"Before walking the tree in in-place mode, DOMPurify runs _isClobbered on the root node to detect DOM clobbering attacks (properties like node.name/attributes overwritten by named elements/ids, or broken form associations). A clobbered root cannot be trusted, and because in-place mode cannot detach or replace the root, DOMPurify neutralizes it (using clobber-safe cached getters) and throws this TypeError, fail-closed (see GHSA-r47g-fvhr-h676).","triggerScenarios":"Calling DOMPurify.sanitize(rootElement, { IN_PLACE: true }) where the root element itself is clobbered — e.g. a <form> or <img> with a name/id that shadows document properties, or whose named descendants (form, image, attributes collections) collide with node internals.","commonSituations":"Sanitizing legacy markup with named forms/inputs (<form name=\"attributes\">, <img name=\"nodeType\">); user-generated content embedded with name/id attributes chosen to shadow DOM APIs; migrating legacy pages into in-place sanitization.","solutions":["Remove or rename the clobbering name/id attributes on the root element before sanitizing (rename to non-colliding values).","Avoid in-place mode for clobber-prone content: serialize with outerHTML and sanitize the string so DOMPurify can rebuild a clean tree.","Strip name/id attributes via a SANITIZE hook or preprocessing (e.g. ALLOW_DATA_ATTR off, FORBID_ATTR: ['name']) for untrusted content.","Catch the TypeError and treat the root as untrusted content: discard it or re-create it from sanitized markup."],"exampleFix":"// before: clobbered root, e.g. <form name=\"attributes\">...\nDOMPurify.sanitize(formEl, { IN_PLACE: true }); // throws\n// after\nformEl.removeAttribute('name'); // de-clobber first\nDOMPurify.sanitize(formEl, { IN_PLACE: true });\n// or rebuild from string\nconst safe = DOMPurify.sanitize(formEl.outerHTML);","handlingStrategy":"validation","validationCode":"function looksClobbered(el) {\n  return (el.attributes && el.attributes.length) !== el.attributes.length ||\n         el.querySelector && !!el.querySelector('[name=attributes],[name=nodeType],[name=tagName]');\n}\nif (root.hasAttribute && root.hasAttribute('name')) root.removeAttribute('name');","typeGuard":"const isClobberSafeRoot = (n) => n instanceof Element && !n.hasAttribute('name') && !/^(attributes|nodeType|tagName|parentNode)$/.test(n.id || '');","tryCatchPattern":"try {\n  DOMPurify.sanitize(root, { IN_PLACE: true });\n} catch (e) {\n  if (String(e.message).includes('root node is clobbered')) {\n    const safe = DOMPurify.sanitize(root.outerHTML); // fail closed, rebuild\n  } else { throw e; }\n}","preventionTips":["Strip name/id attributes from untrusted roots before in-place sanitization.","Never sanitize legacy markup with named forms/images in IN_PLACE mode; rebuild from strings.","Audit user content for DOM-clobbering patterns (name/id shadowing document properties).","Prefer RETURN_DOM/string sanitization over IN_PLACE for untrusted trees."],"tags":["dompurify","dom-sanitization","dom-clobbering","in-place","security"],"backgroundTag":"dom-clobbering-detected","analyzedSha":"470040131122bd44e269b4de0f2e9557f90ec994","analyzedAt":"2026-09-02T10:19:15.878Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T16:17:10.729Z"}