{"record":{"id":"96c6ede73680a91f","repo":"basecamp/trix","slug":"a-configured-trusted-types-policy-callback-create","errorCode":null,"errorMessage":"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)","messagePattern":"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\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"action_text-trix/app/assets/javascripts/trix.js","lineNumber":2253,"sourceCode":"    let trustedTypesPolicy;\n    let emptyHTML = '';\n    // The instance's own internal Trusted Types policy. Unlike a caller-supplied\n    // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws\n    // on duplicate policy names — and is the only policy allowed to persist\n    // across configurations and survive `clearConfig()`.\n    let defaultTrustedTypesPolicy;\n    let defaultTrustedTypesPolicyResolved = false;\n    // Tracks whether we are already inside a call to the configured Trusted Types\n    // policy (`createHTML` or `createScriptURL`). If a supplied policy callback\n    // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would\n    // re-enter the policy and recurse until the stack overflows. We detect that\n    // re-entry and throw a clear, actionable error instead. The guard is shared\n    // across both callbacks, because either one re-entering `sanitize` triggers\n    // the same unbounded recursion.\n    let IN_TRUSTED_TYPES_POLICY = 0;\n    const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {\n      if (IN_TRUSTED_TYPES_POLICY > 0) {\n        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.');\n      }\n    };\n    const _createTrustedHTML = function _createTrustedHTML(html) {\n      _assertNotInTrustedTypesPolicy();\n      IN_TRUSTED_TYPES_POLICY++;\n      try {\n        return trustedTypesPolicy.createHTML(html);\n      } finally {\n        IN_TRUSTED_TYPES_POLICY--;\n      }\n    };\n    const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {\n      _assertNotInTrustedTypesPolicy();\n      IN_TRUSTED_TYPES_POLICY++;\n      try {\n        return trustedTypesPolicy.createScriptURL(scriptUrl);\n      } finally {\n        IN_TRUSTED_TYPES_POLICY--;","sourceCodeStart":2235,"sourceCodeEnd":2271,"githubUrl":"https://github.com/basecamp/trix/blob/470040131122bd44e269b4de0f2e9557f90ec994/action_text-trix/app/assets/javascripts/trix.js#L2235-L2271","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: policy re-enters sanitize -> infinite recursion\nconst policy = {\n  createHTML: (dirty) => DOMPurify.sanitize(dirty),\n  createScriptURL: (s) => s\n};\nDOMPurify.sanitize(input, { TRUSTED_TYPES_POLICY: policy });\n// after: policy is a pure signer\nconst policy = {\n  createHTML: (clean) => clean,\n  createScriptURL: (s) => s\n};\nDOMPurify.sanitize(input, { TRUSTED_TYPES_POLICY: policy, RETURN_TRUSTED_TYPE: true });","handlingStrategy":"validation","validationCode":"function assertPolicyIsPure(policy) {\n  const src = String(policy.createHTML);\n  if (src.includes('DOMPurify.sanitize') || src.includes('DOMPurify')) {\n    throw new Error('TRUSTED_TYPES_POLICY.createHTML must not call DOMPurify.sanitize');\n  }\n  if (typeof policy.createScriptURL !== 'function' || typeof policy.createHTML !== 'function') {\n    throw new Error('policy must define createHTML and createScriptURL');\n  }\n}\n// call before: assertPolicyIsUseable(myPolicy);","typeGuard":"const isPurePolicy = (p) =>\n  !!p && typeof p.createHTML === 'function' && typeof p.createScriptURL === 'function' &&\n  !String(p.createHTML).includes('sanitize');","tryCatchPattern":"try {\n  DOMPurify.sanitize(dirty, { TRUSTED_TYPES_POLICY: policy, RETURN_TRUSTED_TYPE: true });\n} catch (e) {\n  if (String(e.message).includes('infinite recursion')) {\n    console.error('Policy re-enters sanitize; replace with a pure signer policy.');\n  } else { throw e; }\n}","preventionTips":["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."],"tags":["dompurify","trusted-types","infinite-recursion","configuration"],"backgroundTag":"infinite-recursion-in-callback","analyzedSha":"470040131122bd44e269b4de0f2e9557f90ec994","analyzedAt":"2026-09-02T10:19:15.878Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T16:17:10.729Z"}