Summary
quill@2.0.3 is flagged by a Dependabot / npm audit advisory for an XSS issue in its
HTML-export path (GHSA-v3m3-f69x-jf25). There is no patched upstream release of Quill
yet — npm audit fix --force only "resolves" it by downgrading to quill@2.0.2 (an older
release, not a real fix), which we deliberately do not take.
This issue tracks the advisory and documents the mitigation we ship in the meantime.
Why the practical risk is low
Quill's rich text only reaches a viewer's browser through the agreement template body, which
is authored by an authenticated inspector/admin (in standalone, the operator themselves) — not
by anonymous/untrusted users. The advisory is exploitable only when malicious HTML is rendered to a
victim, so the realistic attack surface is small.
Mitigation shipped (defense-in-depth)
The render side is hardened so that whether or not Quill produces unsafe markup, nothing dangerous
can reach the DOM:
1. Server-side sanitize at write time — server/services/agreement.service.ts
(sanitizeAgreementHtml), applied in createAgreement / updateAgreement before persisting:
- Strips dangerous element pairs and their content (
script, style, iframe, object, embed,
form, input, button, textarea, svg, math, link, meta).
- Removes HTML comments in a loop-until-stable pass (guards against reconstructed
<!-- payloads —
CodeQL js/incomplete-multi-character-sanitization).
- Allow-lists only
p, strong, em, u, b, i, h2, h3, ol, ul, li, br, span. Allowed tags are
rebuilt clean — every attribute is dropped except a strictly-matched class="ql-…" on
p/ol/ul/li (for Quill indent levels).
- Loops a final
on* / javascript: / data: strip until stable.
2. Client-side DOMPurify on mount — app/components/SanitizedHtml.tsx:
- Lazy
import('dompurify') inside useEffect (≈26 KB client-only chunk; worker entry size
unchanged), re-sanitizing with a real HTML parser using an allow-list mirroring the server.
- Cloudflare Workers SSR has no DOM, so DOMPurify runs only in the browser (and in the headless
renderer used for signed PDFs). SSR emits the server-sanitized HTML as a safe first paint /
no-JS fallback.
Both user-content render paths — app/routes/public/agreement-sign.tsx and
app/routes/public/agreement-printable.tsx — go through <SanitizedHtml>. (The only other
dangerouslySetInnerHTML usages are static CSS / FOUC constants in root.tsx, not user input.)
Why this is structurally safe
The output allow-list contains no URL-bearing tags — no <a>, no <img>, no <svg>. Combined
with the rebuild-without-attributes approach, event-handler attributes and javascript:/data:
URIs are structurally unreachable, and <script> injected via innerHTML is inert. The security
therefore rests on the restrictive allow-list, not on regex precision.
⚠️ Regression guard: do not add <a href> or <img src> to the allow-list (in either
SanitizedHtml.tsx ALLOWED_TAGS or agreement.service.ts sanitizeAgreementHtml). Doing so
would make the weaker regex javascript:/data: strip the only remaining guard on the SSR/no-JS
path.
Resolution criteria
References
- Advisory: GHSA-v3m3-f69x-jf25
- Mitigation:
server/services/agreement.service.ts, app/components/SanitizedHtml.tsx
Summary
quill@2.0.3is flagged by a Dependabot /npm auditadvisory for an XSS issue in itsHTML-export path (GHSA-v3m3-f69x-jf25). There is no patched upstream release of Quill
yet —
npm audit fix --forceonly "resolves" it by downgrading toquill@2.0.2(an olderrelease, not a real fix), which we deliberately do not take.
This issue tracks the advisory and documents the mitigation we ship in the meantime.
Why the practical risk is low
Quill's rich text only reaches a viewer's browser through the agreement template body, which
is authored by an authenticated inspector/admin (in standalone, the operator themselves) — not
by anonymous/untrusted users. The advisory is exploitable only when malicious HTML is rendered to a
victim, so the realistic attack surface is small.
Mitigation shipped (defense-in-depth)
The render side is hardened so that whether or not Quill produces unsafe markup, nothing dangerous
can reach the DOM:
1. Server-side sanitize at write time —
server/services/agreement.service.ts(
sanitizeAgreementHtml), applied increateAgreement/updateAgreementbefore persisting:script,style,iframe,object,embed,form,input,button,textarea,svg,math,link,meta).<!--payloads —CodeQL
js/incomplete-multi-character-sanitization).p, strong, em, u, b, i, h2, h3, ol, ul, li, br, span. Allowed tags arerebuilt clean — every attribute is dropped except a strictly-matched
class="ql-…"onp/ol/ul/li(for Quill indent levels).on*/javascript:/data:strip until stable.2. Client-side DOMPurify on mount —
app/components/SanitizedHtml.tsx:import('dompurify')insideuseEffect(≈26 KB client-only chunk; worker entry sizeunchanged), re-sanitizing with a real HTML parser using an allow-list mirroring the server.
renderer used for signed PDFs). SSR emits the server-sanitized HTML as a safe first paint /
no-JS fallback.
Both user-content render paths —
app/routes/public/agreement-sign.tsxandapp/routes/public/agreement-printable.tsx— go through<SanitizedHtml>. (The only otherdangerouslySetInnerHTMLusages are static CSS / FOUC constants inroot.tsx, not user input.)Why this is structurally safe
The output allow-list contains no URL-bearing tags — no
<a>, no<img>, no<svg>. Combinedwith the rebuild-without-attributes approach, event-handler attributes and
javascript:/data:URIs are structurally unreachable, and
<script>injected viainnerHTMLis inert. The securitytherefore rests on the restrictive allow-list, not on regex precision.
Resolution criteria
double-sanitize layer can be relaxed.
References
server/services/agreement.service.ts,app/components/SanitizedHtml.tsx