Security¶
The editor stores HTML and you render it with |safe, so the boundary matters. The
design makes |safe defensible:
- ProseMirror's schema is a sanitizer. Content is parsed into a strict document
model on load; anything the schema doesn't represent —
<script>, event handlers, unknown tags/attributes — is dropped, not stored. The editor never holds markup it can't model. - Link protocols are allowlisted (
linkProtocols, defaulthttp/https/mailto/tel).javascript:and other schemes are stripped on parse. - Image
srcis protocol-validated — both the uploadlocationand any pickervalueare checked (http/https/data) before insertion;javascript:is refused. - Source view re-parses through the schema. Pasting raw HTML into the source view and switching back normalizes anything unsupported away — what you see equals what is stored.
What survives¶
The serialized output is limited to the configured nodes/marks: paragraphs and headings
(with alignment/margin), bold/italic/underline/strike/code, sub/superscript, font
size/family/colour/highlight (as <span style>), links (allowlisted), images, tables,
lists, blockquotes, and horizontal rules. Everything else normalizes away.
JSON storage¶
JSON storage (TipTapJSONField) keeps the same boundary, with one extra rule.
Because protocol allowlisting happens on parse — which a stored-JSON document never runs —
rendering arbitrary JSON is not automatically safe. So:
- The field validates the stored
docon every save: the link/image protocol allowlist is enforced in pure Python (no extra dependency), and disallowedjavascript:/vbscript:/other schemes on linkhref/ imagesrcare stripped. The canonical value is always safe, whoever wrote it (form, API, import). - The
htmlmirror is re-derived from the sanitizeddocon every save (never trusted from the caller) by the built-inrender_doc, which re-applies the protocol allowlist, HTML-escapes text and attributes, and passes inlinestylevalues through a conservative CSS allowlist (no;/:injection, nourl(...:...), noexpression). A write can set{doc, html}directly (API / import / hand-edit); the suppliedhtmlis discarded, so the rendered surface always reflects only the sanitized doc. The field marks.htmlsafe on that basis.render_docis also available directly and via thetiptap_htmlfilter for rendering a baredoc.
Caveats¶
- You still control the render context.
|safetrusts the stored HTML; keep untrusted input out of attributes you interpolate around it. - Custom extensions own their sanitization. Any new node/attribute a custom extension introduces widens the surface — validate what it accepts.
- External asset mode loads TipTap you provide; the guarantees above hold for the pinned, bundled version. See Asset modes.
- Uploads are yours to police.
BaseImageUploadViewenforces the wire contract, not file-type/size/virus policy — add those insave.