We take security issues seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
This project doesn’t have formal support targets for non-latest versions. Backporting security fixes to affected releases will be decided on a case-by-case basis, based on effort involved and known usage of affected versions.
To report a vulnerability, please follow Wagtail’s security reporting guidance.
draftjs_exporter converts a Draft.js ContentState (blocks, inline style ranges, entity ranges, an entity map) into an HTML or Markdown string, via a pluggable rendering engine (string, html5lib, lxml, markdown) and a set of maps/decorators/entity handlers supplied by the integrating application.
Two things cross a trust boundary on every call:
ContentStateis the untrusted input. In a typical integration (like Wagtail), it comes from a rich text editor or external API integration, might be stored in a database, and later re-rendered for arbitrary site visitors. Soo it must be treated as attacker-controlled, including block text,inlineStyleRanges,entityRanges, and thedatapayload of each entity.- The rendered HTML string is the output, which is often embedded directly into a page served to end users without further escaping. Anything that reaches that string unescaped is a potential vector for those end users.
By contrast, the ExporterConfig (block_map, style_map, entity_decorators, composite_decorators, engine) is developer-supplied, trusted code, not attacker input. The library assumes these are wired up by the integrating application, not derived from request data. As a diagram:
Untrusted zone Trusted zone Untrusted zone
┌──────────────────┐ render() ┌──────────────────────┐ string ┌────────────────────┐
│ ContentState │ ───────────▶│ HTML / DOM / engines │─────────▶│ Rendered HTML page │
│ (blocks, styles, │ │ (escaping happens │ │ (viewed by other │
│ entities, data) │ │ here) │ │ end users) │
└──────────────────┘ └──────────────────────┘ └────────────────────┘
▲
│ configures
┌──────────────────────────┐
│ ExporterConfig │
│ (block/style maps, │
│ entity decorators, │
│ engine, developer code) │
└──────────────────────────┘Out of scope. Depends entirely on the integrating application.
- Entity
datais passed straight through into element attributes.EntityState.render_entitiescopiesentity_details["data"]into the props of the element the entity decorator renders, e.g. aLINKentity'surltypically becomes anhref. Attribute values are HTML-escaped by every engine (html.escapein thestringengine, BeautifulSoup/lxml elsewhere), which prevents attribute/quote breakout, but the library doesn't validate URL schemes. A malicious"data": {"url": "javascript:alert(1)"}will render ashref="javascript:alert(1)"verbatim. Entity decorators that emithref,src, or similar URL-bearing attributes must validate the scheme (allow-listhttp/https/mailto) themselves. styleprops are interpolated without CSS validation. A style dict is converted to an inlinestyle="..."string by joining property/value pairs; values aren’t validated as safe CSS, only escaped for the surrounding attribute quotes. Entity/decorator code that forwards untrusteddataintostyleprops can inject arbitrary CSS declarations (e.g.url()-based exfiltration).DOM.parse_html()/Elt.from_html()intentionally bypass escaping. This exists so integrators can render pre-sanitized, developer-controlled HTML fragments. If a decorator instead feeds it attacker-controlled text (e.g. raw entity data), that text is emitted into the page unescaped. That’s a stored-XSS vector. Never callparse_htmlwith data sourced from ContentState.
The library does no logging and keeps no audit trail. Those can be added by implementers. Exceptions (EntityException, WrapperException, etc.) carry entity keys and, indirectly, fragments of the input; applications that surface these messages to end users (like via debug pages) could leak content that should not be exposed. Treat exporter exceptions as internal diagnostics and avoid exposing raw exception messages in production error pages.
The exporter does not perform network or filesystem access itself. Parsing raw markup with the lxml engine (html.fromstring) does not resolve external entities/DTDs by default, so classic XXE-style disclosure is not applicable there. The main disclosure risk is the exception-message leakage described under Repudiation, and, more generally, whatever an integrator's own entity decorators choose to do with entity data.
ContentState should be treated as untrusted input. Please report any input that evades the project’s property-based tests that assess handling of anomalous content.
Another possible vector is composite_decorators strategies, that use developer-supplied regex patterns running against attacker-controlled block text on every render (composite_decorators.py). Keep decorator regexes simple and anchored to avoid ReDoS, and enforce your own ContentState size/depth limits upstream if you accept documents from untrusted users.
This is out of scope as long as the exporter configuration is defined in code rather than from user input.
- Never construct exporter options from user/request data. Treat them as trusted, developer-authored configuration only.
- In every entity decorator that emits a URL-bearing attribute (
href,src,poster, …), validate the scheme against an allow-list before rendering it. - Avoid
parse_html()with anything derived from untrusted input. Reserve it for static, developer-controlled markup. - Keep composite decorator
strategyregexes simple and anchored, since they run against attacker-controlled block text on every render. - Don’t surface raw exporter exception messages to end users; log them internally instead.
- If accepting arbitrarily large ContentState documents from untrusted users, enforce your own size/depth limits upstream of the exporter.