basecamp/trix · critical
A configured TRUSTED_TYPES_POLICY callback (createHTML or cr
Error message
A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section (truncated in region)
What it means
DOMPurify tracks re-entrancy with an IN_TRUSTED_TYPES_POLICY counter that is incremented while a user-supplied TRUSTED_TYPES_POLICY createHTML/createScriptURL callback runs. If such a callback calls DOMPurify.sanitize, sanitize would invoke the policy again, recursing without bound. The shared guard detects the re-entry and throws this TypeError immediately instead of blowing the stack.
Source
Thrown at action_text-trix/app/assets/javascripts/trix.js:2253
let trustedTypesPolicy;
let emptyHTML = '';
// The instance's own internal Trusted Types policy. Unlike a caller-supplied
// `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
// on duplicate policy names — and is the only policy allowed to persist
// across configurations and survive `clearConfig()`.
let defaultTrustedTypesPolicy;
let defaultTrustedTypesPolicyResolved = false;
// Tracks whether we are already inside a call to the configured Trusted Types
// policy (`createHTML` or `createScriptURL`). If a supplied policy callback
// itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
// re-enter the policy and recurse until the stack overflows. We detect that
// re-entry and throw a clear, actionable error instead. The guard is shared
// across both callbacks, because either one re-entering `sanitize` triggers
// the same unbounded recursion.
let IN_TRUSTED_TYPES_POLICY = 0;
const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
if (IN_TRUSTED_TYPES_POLICY > 0) {
throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or ' + 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' + 'infinite recursion. Do not pass a policy whose callbacks wrap ' + 'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' + 'Types" section of the README.');
}
};
const _createTrustedHTML = function _createTrustedHTML(html) {
_assertNotInTrustedTypesPolicy();
IN_TRUSTED_TYPES_POLICY++;
try {
return trustedTypesPolicy.createHTML(html);
} finally {
IN_TRUSTED_TYPES_POLICY--;
}
};
const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
_assertNotInTrustedTypesPolicy();
IN_TRUSTED_TYPES_POLICY++;
try {
return trustedTypesPolicy.createScriptURL(scriptUrl);
} finally {
IN_TRUSTED_TYPES_POLICY--;View on GitHub (pinned to 4700401311)
Solutions
- Remove the DOMPurify.sanitize call from inside the policy callback; a createHTML callback must be a pure signer (e.g. identity or pass-through to trustedTypes), not a sanitizer.
- Do any sanitizing before/outside the policy, and let the policy only transform the already-sanitized string.
- Read the 'DOMPurify and Trusted Types' section of the README and follow its recommended policy shape.
- If recursive sanitization is genuinely needed, restructure it with two separate DOMPurify instances/entry points so the policy never re-enters sanitize.
Example fix
// before: policy re-enters sanitize -> infinite recursion
const policy = {
createHTML: (dirty) => DOMPurify.sanitize(dirty),
createScriptURL: (s) => s
};
DOMPurify.sanitize(input, { TRUSTED_TYPES_POLICY: policy });
// after: policy is a pure signer
const policy = {
createHTML: (clean) => clean,
createScriptURL: (s) => s
};
DOMPurify.sanitize(input, { TRUSTED_TYPES_POLICY: policy, RETURN_TRUSTED_TYPE: true }); Defensive patterns
Strategy: validation
Validate before calling
function assertPolicyIsPure(policy) {
const src = String(policy.createHTML);
if (src.includes('DOMPurify.sanitize') || src.includes('DOMPurify')) {
throw new Error('TRUSTED_TYPES_POLICY.createHTML must not call DOMPurify.sanitize');
}
if (typeof policy.createScriptURL !== 'function' || typeof policy.createHTML !== 'function') {
throw new Error('policy must define createHTML and createScriptURL');
}
}
// call before: assertPolicyIsUseable(myPolicy); Type guard
const isPurePolicy = (p) =>
!!p && typeof p.createHTML === 'function' && typeof p.createScriptURL === 'function' &&
!String(p.createHTML).includes('sanitize'); Try / catch
try {
DOMPurify.sanitize(dirty, { TRUSTED_TYPES_POLICY: policy, RETURN_TRUSTED_TYPE: true });
} catch (e) {
if (String(e.message).includes('infinite recursion')) {
console.error('Policy re-enters sanitize; replace with a pure signer policy.');
} else { throw e; }
} Prevention
- Keep policy callbacks pure: identity or allow-list mapping only — never sanitize inside them.
- Do all sanitization before the value reaches the policy.
- Follow the 'DOMPurify and Trusted Types' README section when writing policies.
- Never share a sanitize helper between app code and policy callbacks.
When it happens
Trigger: Passing a TRUSTED_TYPES_POLICY whose createHTML (or createScriptURL) implementation itself calls DOMPurify.sanitize on its input, then calling DOMPurify.sanitize with RETURN_TRUSTED_TYPE (or any path that signs output through the policy).
Common situations: Developers copying an example where the policy 'wraps' sanitize to double-sanitize; refactoring sanitize() into a helper used both in app code and inside the policy callback; confusing TRUSTED_TYPES_POLICY with a normal afterSanitize hook and putting sanitization logic there.
Related errors
- TRUSTED_TYPES_POLICY configuration option must provide a "cr
- TRUSTED_TYPES_POLICY configuration option must provide a "cr
- TrustedTypes policy ' + policyName + ' could not be created.
- a node selected for removal could not be detached from its t
- dirty is not a string, aborting
AI-assisted analysis of basecamp/trix@4700401311 (2026-09-02).
Data as JSON: /api/errors/96c6ede73680a91f.
Report an issue: GitHub.