Sanitizer and parser options resolve through the prototype chain
HTMLSanitizer and HTMLParser take their security-relevant settings as destructured options with a {} default:
// src/trix/models/html_sanitizer.js
constructor(html, { allowedAttributes, forbiddenProtocols, forbiddenElements, purifyOptions } = {}) {
this.allowedAttributes = allowedAttributes || DEFAULT_ALLOWED_ATTRIBUTES
this.forbiddenProtocols = forbiddenProtocols || DEFAULT_FORBIDDEN_PROTOCOLS
this.forbiddenElements = forbiddenElements || DEFAULT_FORBIDDEN_ELEMENTS
this.purifyOptions = purifyOptions || {}// src/trix/models/html_parser.js
constructor(html, { referenceElement, purifyOptions } = {}) {Destructuring is a property read, so each name resolves along the prototype chain. Our own call sites pass either no options object or a literal without these keys (views/attachment_view.js, models/editor.js, models/composition.js, core/serialization.js), so if Object.prototype carries any of these four names, that value wins over the defaults.
purifyOptions is the one with the most reach, because sanitize() does:
const purifyConfig = Object.assign({}, config.dompurify, this.purifyOptions)
DOMPurify.setConfig(purifyConfig)An inherited value is merged over Trix.config.dompurify, so an embedder's hardening is overridden rather than combined with. And since setConfig() is called without a matching clearConfig(), that configuration persists as the residual state later read by DOMPurify.isValidAttribute in models/string_piece.js, views/attachment_view.js and controllers/toolbar_controller.js.
Scope, stated plainly
This is defence in depth, not a directly exploitable issue. Reaching it requires a prototype-pollution primitive from somewhere else on the page, and anything that can write to Object.prototype already has script execution, at which point the sanitizer is not what stands between an attacker and the user. We are not aware of a gadget in Trix that provides such a write, and we checked one candidate specifically: it replaces the prototype of an internal object only and leaves Object.prototype untouched.
We are filing it anyway, because reading security-relevant configuration through the prototype chain is a poor default even when nothing can currently reach it, and it makes Trix needlessly fragile for embedders who may have such a primitive in their own application.
Suggested direction
Resolve these options without consulting the prototype chain, for example by taking the options object as a whole and checking Object.hasOwn(options, key) before use, or by normalising it through Object.assign(Object.create(null), options) at the top of each constructor. Worth applying the same treatment to HTMLParser's referenceElement and purifyOptions, and considering clearConfig() after sanitizing so DOMPurify state does not persist between calls.
Credit
Reported to our HackerOne program by @the11gh0st, who mapped each affected key to the specific defence it disables. Thanks for a clear, well-evidenced report.
Source: basecamp/trix