basecamp/trix · error · TypeError
root node is forbidden and cannot be sanitized in-place
Error message
root node is forbidden and cannot be sanitized in-place
What it means
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.
Source
Thrown at action_text-trix/app/assets/javascripts/trix.js:4019
if (inPlace) {
/* Declarative-partial-updates / streaming pre-pass: sever every patch
linkage across the live tree BEFORE the walk, so no patch can fire
mid-walk and inject into an already-processed region. Runs first, so
it also covers the forbidden/clobbered roots that throw below. */
_neutralizePatchLinkage(dirty);
/* Do some early pre-sanitization to avoid unsafe root nodes.
Read nodeName through the cached prototype getter — a clobbering
child named "nodeName" on the form root would otherwise shadow
the property and let this check skip the root-allowlist
validation entirely. */
const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
if (typeof nn === 'string') {
const tagName = transformCaseFunc(nn);
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
/* Fail closed on a live root: neutralize handlers/children before
throwing, exactly as the mid-walk abort path does. */
_neutralizeRoot(dirty);
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');View on GitHub (pinned to 4700401311)
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.
Example fix
// before
DOMPurify.sanitize(customEl, { IN_PLACE: true, ADD_TAGS: [] }); // throws: root forbidden
// after
DOMPurify.sanitize(customEl, { IN_PLACE: true, ADD_TAGS: ['my-widget'] });
// or sanitize an allowed wrapper instead
DOMPurify.sanitize(wrapperDiv, { IN_PLACE: true }); Defensive patterns
Strategy: validation
Validate before calling
const tag = root.tagName.toLowerCase();
const cfg = { ALLOWED_TAGS: DOMPurify.defaults.ALLOWED_TAGS /* + your additions */ };
if (!cfg.ALLOWED_TAGS.includes(tag)) {
// swap root for an allowed wrapper or add the tag via ADD_TAGS before calling
} Type guard
const isAllowedRoot = (n, allowedTags) => n instanceof Element && allowedTags.includes(n.tagName.toLowerCase());
Try / catch
try {
DOMPurify.sanitize(root, { IN_PLACE: true, ADD_TAGS: ['my-widget'] });
} catch (e) {
if (String(e.message).includes('root node is forbidden')) {
const safe = DOMPurify.sanitize(root.outerHTML); // rebuild from string
} else { throw e; }
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- a node selected for removal could not be detached from its t
- 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/9d553b423a044830.
Report an issue: GitHub.