basecamp/trix · error · TypeError

root node is clobbered and cannot be sanitized in-place

Error message

root node is clobbered and cannot be sanitized in-place

What it means

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).

Source

Thrown at action_text-trix/app/assets/javascripts/trix.js:4037

            throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
          }
        }
        /* Pre-flight the root through _isClobbered. The iterator-driven
           removal path can not detach a parent-less root: _forceRemove
           falls through to Element.prototype.remove(), which per spec
           is a no-op on a node with no parent. A clobbered root would
           then survive the main loop with its attributes uninspected,
           because _sanitizeAttributes early-returns on _isClobbered. The
           result would be an attacker-controlled form, complete with any
           event-handler attributes the caller passed in, handed back to
           the application unsanitized. Refuse to sanitize such a root
           the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
        if (_isClobbered(dirty)) {
          /* Fail closed on a live clobbered root before throwing.
             _neutralizeRoot's reads are clobber-safe (cached getters); the
             form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
          _neutralizeRoot(dirty);
          throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
        }
        /* Sanitize attached shadow roots before the main iterator runs.
           The iterator does not descend into shadow trees. Same fail-closed
           barrier as the main walk (campaign-3 F2): a custom-element reaction
           inside a shadow root could abort this pre-pass before the walk runs,
           which would otherwise leave the entire live tree unsanitized. */
        try {
          _sanitizeAttachedShadowRoots(dirty);
        } catch (error) {
          _neutralizeRoot(dirty);
          throw error;
        }
      } else if (_isNode(dirty)) {
        /* If dirty is a DOM element, append to an empty document to avoid
           elements being stripped by the parser */
        body = _initDocument('<!---->');
        importedNode = body.ownerDocument.importNode(dirty, true);
        if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {

View on GitHub (pinned to 4700401311)

Solutions

  1. Remove or rename the clobbering name/id attributes on the root element before sanitizing (rename to non-colliding values).
  2. Avoid in-place mode for clobber-prone content: serialize with outerHTML and sanitize the string so DOMPurify can rebuild a clean tree.
  3. Strip name/id attributes via a SANITIZE hook or preprocessing (e.g. ALLOW_DATA_ATTR off, FORBID_ATTR: ['name']) for untrusted content.
  4. Catch the TypeError and treat the root as untrusted content: discard it or re-create it from sanitized markup.

Example fix

// before: clobbered root, e.g. <form name="attributes">...
DOMPurify.sanitize(formEl, { IN_PLACE: true }); // throws
// after
formEl.removeAttribute('name'); // de-clobber first
DOMPurify.sanitize(formEl, { IN_PLACE: true });
// or rebuild from string
const safe = DOMPurify.sanitize(formEl.outerHTML);
Defensive patterns

Strategy: validation

Validate before calling

function looksClobbered(el) {
  return (el.attributes && el.attributes.length) !== el.attributes.length ||
         el.querySelector && !!el.querySelector('[name=attributes],[name=nodeType],[name=tagName]');
}
if (root.hasAttribute && root.hasAttribute('name')) root.removeAttribute('name');

Type guard

const isClobberSafeRoot = (n) => n instanceof Element && !n.hasAttribute('name') && !/^(attributes|nodeType|tagName|parentNode)$/.test(n.id || '');

Try / catch

try {
  DOMPurify.sanitize(root, { IN_PLACE: true });
} catch (e) {
  if (String(e.message).includes('root node is clobbered')) {
    const safe = DOMPurify.sanitize(root.outerHTML); // fail closed, rebuild
  } else { throw e; }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of basecamp/trix@4700401311 (2026-09-02). Data as JSON: /api/errors/f41606e238f74e05. Report an issue: GitHub.