basecamp/trix · error · TypeError
a node selected for removal could not be detached from its t
Error message
a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place
What it means
During the DOM walk, when a node must be force-removed, DOMPurify calls remove(node) and then checks getParentNode(node). A detached, parentless root node cannot be removed via removeChild/remove (Element.remove() is a no-op without a parent), so DOMPurify cannot guarantee the node is actually detached from any live tree. Since the caller asked for in-place sanitization, returning a still-attached/unsafe node would be unsafe, so it throws this TypeError instead of returning anything.
Source
Thrown at action_text-trix/app/assets/javascripts/trix.js:2853
.remove() is itself a spec no-op on a parentless node, so a recorded
"removal" would otherwise hand the caller back an intact,
payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
the style-with-element-child rule decided to kill). Fail closed by
throwing — exactly as a clobbered root does at the IN_PLACE entry —
rather than trying to "neutralize" the node via its own methods.
Neutralizing would mean calling getAttributeNames()/removeAttribute()
on the node, both of which a <form> root can clobber via a named child
(and _isClobbered does not even probe getAttributeNames), so the
neutralize step could itself be silently defeated, leaving the payload
intact. A throw touches only the cached, clobber-safe remove() and
getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
to every root-kill reason. REPORT-3.
This lives inside the catch, so it never fires for a normally-removed
in-tree node: those have a parent, removeChild() succeeds, and the
catch is not entered. Only a kept (parentless) root reaches here. */
remove(node);
if (!getParentNode(node)) {
throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
}
}
};
/**
* _neutralizeRoot
*
* Fail-closed teardown of an in-place root after the sanitize walk aborts
* (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
* custom element's reaction detaches a node so `_forceRemove`'s deliberate
* parentless guard throws, or any other re-entrant engine mutation — would
* otherwise leave the caller's *live* tree half-sanitized, with everything
* after the abort point still carrying its handlers. There is no safe way
* to resume the walk (the tree mutated under us), so we strip the root bare:
* remove every child and every attribute, then let the caller's catch see
* the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
* getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
*
* @param root the in-place root to emptyView on GitHub (pinned to 4700401311)
Solutions
- Do not pass the problematic node as the in-place root; instead sanitize its serialized HTML: DOMPurify.sanitize(node.outerHTML) and rebuild the element from the result.
- Append the node to a temporary detached container with a parent before in-place sanitization, so removal can succeed.
- Check beforehand whether the root tag is allowed (ALLOWED_TAGS/ADD_TAGS) or the element is clobbered, and replace it before calling sanitize.
- Catch this TypeError and fall back to string-based sanitization.
Example fix
// before: detached root sanitized in place
DOMPurify.sanitize(el, { RETURN_DOM: true, IN_PLACE: true }); // throws if el must be removed
// after: sanitize via serialized HTML
const clean = DOMPurify.sanitize(el.outerHTML);
const safeEl = DOMPurify.sanitize(clean, { RETURN_DOM: true }); Defensive patterns
Strategy: validation
Validate before calling
if (root.parentNode === null && needsInPlaceSanitize) {
// detached root: fall back to string sanitization
const safeHtml = DOMPurify.sanitize(root.outerHTML);
}
// also pre-check the root tag is allowed:
const tag = root.tagName.toLowerCase();
if (!allowedTags.includes(tag)) { /* replace root before IN_PLACE sanitize */ } Type guard
const isSafelyInPlaceSanitizable = (n) => n instanceof Node && n.parentNode !== null && typeof n.remove === 'function';
Try / catch
try {
DOMPurify.sanitize(root, { IN_PLACE: true });
} catch (e) {
if (String(e.message).includes('could not be detached')) {
const safe = DOMPurify.sanitize(root.outerHTML); // fallback path
} else { throw e; }
} Prevention
- Only use IN_PLACE on nodes that are attached (have a parent).
- Verify the root tag is in the allowlist before in-place sanitizing.
- Prefer string/RETURN_DOM sanitization for detached trees.
- Check for clobbering names/ids on the root before passing it in.
When it happens
Trigger: Calling DOMPurify.sanitize(rootNode, { ... , RETURN_DOM: true } / IN_PLACE mode) where the root itself is selected for removal during the walk (root-kill) AND the node has no parent, so Element.prototype.remove() cannot detach it.
Common situations: Sanitizing a freshly created (not yet appended) element whose root element itself turns out to be forbidden/clobbered; passing a document fragment or detached element as the sanitize root in in-place mode; framework code that sanitizes a live node before mounting it.
Related errors
- root node is forbidden and cannot be sanitized in-place
- root node is clobbered and cannot be sanitized in-place
- TrustedTypes policy ' + policyName + ' could not be created.
- A configured TRUSTED_TYPES_POLICY callback (createHTML or cr
- TRUSTED_TYPES_POLICY configuration option must provide a "cr
AI-assisted analysis of basecamp/trix@4700401311 (2026-09-02).
Data as JSON: /api/errors/1180e690abbd48ca.
Report an issue: GitHub.