Is Quillbot: AI Writing Assistant to Grammar Check, Paraphrase & Translate safe?

Medium risk

QuillBot sends behavioral telemetry including the URLs of pages where it is active to its own Snowplow collector.

When users interact with QuillBot, the extension's service worker transmits structured event data — including user ID, the host origin of the active tab, action names, build version, and session context — to collector.quillbot.com via Snowplow self-describing event payloads. A secondary collector at ol-collector.quillbot.com receives the same data. These events are sent automatically as users type or trigger extension features, without a separate disclosure beyond the extension's privacy policy.

quillbotv4.115.0Chrome Web Store
45Risk
Who publishes it

QuillBot (Course Hero), LLC - 1 other listing from the same operator, 1 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
quillbot
Declared legal entity
QuillBot (Course Hero), LLC
Registered address
303 East Wacker Drive, Suite 2101, Chicago, IL 60601-5223, US
Registered contact
Learneo Inc.

Same operator - 1 listing

Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.

Shared hosts - 2 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

languagetool.com
Also called by 5 other listings, including LanguageTool, Grammar and Spell Checker - LanguageTool
learneo.com
Also called by 5 other listings, including LanguageTool, Grammar and Spell Checker - LanguageTool

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Any quillbot.com or coursehero.com page can request your device ID

QuillBot answers messages from any quillbot.com, coursehero.com, or quillbot.dev page with your device ID and version, no prompt.

It also reads your login cookie.

That ID later showed in a Google ad-conversion request from quillbot.com.

01EvidenceCAUSE EFFECT
What actually happens
You did this

A script on a quillbot.com, coursehero.com, or quillbot.dev page sends a browser message directly to the QuillBot extension.

This works from any script running on those origins, not only QuillBot's own web app code.

The extension did this

The extension's message handler replies with your device ID and the installed extension version.

No prompt or confirmation is shown to you before the reply is sent.

02EvidenceCODE COMPARE
The code that does this

The externally_connectable message handler and the auth-cookie read, shipped vs deobfuscated

What it actually does
The onMessageExternal listener replying with the device IDdeobfuscated/quillbot-sw.js
!function(e) {
  Nn().runtime.onMessageExternal.addListener(function(t, n, r) {
    switch (t.action) {
      case In.EXTERNAL_MSG_DETECT:
        jn("deviceID").then(function(e) {
          r({
            deviceID: e,
            success: !0,
            version: Nn().runtime.getManifest().version
          })
        });
        break;
      case In.EXTERNAL_MSG_INPUT_TEXT:
        t.inputId && jn(t.inputId).then(function(e) {
          r({
            paraphraseInputText: e
          }), Nn().storage.local.remove(t.inputId)
        });
        break;
      // ... additional EXTERNAL_MSG_* cases follow, gated only by
      // manifest.json's externally_connectable.matches allowlist
    }
  })
}
chrome.cookies.getAll reading the quillbot.com auth-token cookiedeobfuscated/quillbot-sw.js
Nn().cookies.getAll({
  domain: In.QB_COOKIE_INCLUDE_URL,   // "quillbot.com"
  name: In.QB_ID_TOKEN_STRING         // "useridtoken"
}).then(function() {
  var e = u(AVe().m(function e(t) {
    var n, r, i, o, a, s, u;
    return AVe().w(function(e) {
      for (;;) switch (e.n) {
        case 0:
          return e.n = 1, rre();
        case 1:
          if (n = e.v, !(t.length > 0)) { e.n = 3; break }
          return o = t[0].value, e.n = 2, Zne();
        // ... reads the cookie's value and stores it for API auth headers
      }
    })
  }())
})
03EvidenceFIELD TABLE
What a page on the allow-listed origins receives, unprompted
FieldValueWhy it matters
Device ID
ec5a8236-afcc-44a7-9d07-665cdfb65787A persistent identifier tied to this specific QuillBot installation, handed to the page on request.
Extension version
4.100.0Confirms which QuillBot build is installed.
Login-token cookie (read internally)
useridtoken=<session JWT>Your quillbot.com auth cookie, read by the service worker for QuillBot's own API calls; not confirmed sent externally in this test.
04EvidenceNETWORK CAPTURE
Captured request
GEThttps://ad.doubleclick.net/activity;src=15491507;u=ec5a8236-afcc-44a7-9d07-665cdfb65787
Observed during dynamic analysis: a Google ad-conversion request fired from quillbot.com moments after the extension had handed the same device ID to the page, carrying that value as the conversion-tracking key (u= parameter).
05EvidenceTHIRD PARTY LIST
Where the device ID ends up
  • ad.doubleclick.net

    Google's ad-conversion tracking network. Received the QuillBot-issued device ID as the conversion key on a request fired from quillbot.com.

06EvidenceARTIFACT
Reproduce it yourself

Sends the same externally-connectable 'detect-extension' message any script on quillbot.com, coursehero.com, or quillbot.dev can send, and logs the device ID and version QuillBot replies with.

RequiresQuillBot for Chrome installed and enabledA tab open on quillbot.com, coursehero.com, or quillbot.dev
quillbot-external-detect-poc.js · js
// Paste into the DevTools Console on a https://quillbot.com, https://coursehero.com,
// or https://quillbot.dev page. No QuillBot login or UI interaction is required.
const EXTENSION_ID = 'iidnbdjijdkbmajdffnidomddglmieko';

chrome.runtime.sendMessage(EXTENSION_ID, { action: 'detect-extension' }, (response) => {
  if (chrome.runtime.lastError) {
    console.log('No response:', chrome.runtime.lastError.message);
    return;
  }
  console.log('QuillBot replied with:', response);
});
How to run it
  1. 1
    Install and enable QuillBot.
  2. 2
    Open quillbot.com in a tab.
  3. 3
    Open DevTools, paste the script into Console.
  4. 4
    Press Enter.
  5. 5
    The response contains your device ID and version, sent without any prompt.
SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Browsing URLs sent to QuillBot analytics on every page visit

QuillBot sends the full URL of every page you visit to ol-collector.quillbot.com automatically.

Two POSTs (hostOrigin, hostUrl) fire within seconds of a new tab, plus your account or device ID, premium status, and build version.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to any web page in Chrome.

The extension did this

The extension sends the full URL and origin of the page you just visited to ol-collector.quillbot.com.

This happens on every navigation, no interaction with the QuillBot UI is required.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://ol-collector.quillbot.com/com.quillbot.extension
HTTP 200. Two POST requests were observed during dynamic analysis on a single navigation to google.com; one request carried 6 Snowplow events, each reporting hostOrigin='https://www.google.com' and hostUrl='https://www.google.com/'. The gzip body was decompressed to reveal the Snowplow self-describing event structure.
Headers
Content-Typeapplication/json
Content-Encodinggzip
Body
Gzip-compressed Snowplow payload_data array. Decompressed, each event includes hostOrigin, hostUrl, anon-id or user ID, authenticated, premium, appMode, extensionType, platform-version, and product fields.
03EvidenceFIELD TABLE
Fields included in every Snowplow event sent by QuillBot
FieldValueWhy it matters
Page URL (hostUrl)
https://www.google.com/The exact URL of the page you are visiting, including path, query string, and any tracking parameters.
Page origin (hostOrigin)
https://www.google.comThe scheme and hostname of the page, identifying which website you visited.
Anonymous device ID (anon-id)
anon-7f3a1e24b9cdA persistent identifier generated on install, stored in chrome.storage. Lets QuillBot link browsing across sessions without an account.
QuillBot user ID
usr_8c2d4b1f3a9eYour QuillBot account ID, sent when you are logged in. Ties browsing events directly to your named account.
Authenticated status
trueWhether you are currently logged in to a QuillBot account.
Premium subscription flag
falseWhether your account holds a paid QuillBot subscription.
Extension build version
4.85.0The installed version of QuillBot.
04EvidenceCODE COMPARE
The code that does this

Service worker code that captures the active-tab URL and configures the collector

What it actually does
// When an event fires and no hostOrigin was passed in the message,
// the service worker looks up the active tab's URL directly.
chrome.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
  let urlContext = {};
  if (!hostOrigin && tabs[0]?.url) {
    const parsed = new URL(tabs[0].url);
    urlContext = {
      hostOrigin: parsed.origin,   // e.g. 'https://www.google.com'
      hostUrl:    parsed.href      // e.g. 'https://www.google.com/search?q=...'
    };
  }
  const payload = {
    authenticated,      // true/false
    'anon-id': anonId,  // persistent anon device ID
    premium,            // paid plan flag
    appMode,            // 'Extension'
    extensionType,
    hostOrigin,         // from caller or active tab
    hostUrl,            // from caller or active tab
    product,
    'platform-version': manifestVersion,
    ...urlContext
  };
  tracker.sendUserMetricEvent({ userID, action: eventName, value: payload });
});

// Collector endpoint (production) and buffer config:
const collectorUrls = { production: 'https://ol-collector.quillbot.com' };
const config = {
  bufferSize:          1_000_000, // 1 MB before forced flush
  flushIntervalMillis: 15_000,    // flush every 15 seconds
  namespace:           'com.quillbot.extension',
  storageType:         'CHROME',  // backed by chrome.storage
};
05EvidenceTHIRD PARTY LIST
Destinations receiving browsing activity events
  • ol-collector.quillbot.com

    QuillBot's Snowplow collector (namespace com.quillbot.extension). Receives every navigation event with the URL and account context; two POSTs observed per navigation.

  • collector.quillbot.com

    Secondary QuillBot analytics collector in the service worker's Snowplow tracker. Both collector.quillbot.com and ol-collector.quillbot.com are run by QuillBot (Course Hero Inc.).

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

QuillBot patches Google Docs canvas and reads full document text

On docs.google.com, QuillBot injects a MAIN-world script replacing five canvas methods and calling Google's undocumented self._docs_annotate_getAnnotatedText to read doc text, returned via qb-gdocs-fullText, not observed leaving the page.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open a document or presentation on docs.google.com.

The extension did this

QuillBot injects a script into the Google Docs page context that replaces five built-in canvas drawing methods.

It also acquires a handle to read the document's full text and returns that text in response to an internal page event.

02EvidenceCODE COMPARE
The code that does this

Canvas prototype methods replaced and the internal Docs text API acquired (main-world-injection.js)

What it actually does
// M is the QuillBot extension id 'oldceeleldhonbafppcapldpdifcinji'
window._docs_annotate_canvas_by_ext = M;

// Five canvas drawing primitives are replaced with wrappers that record
// stroke geometry (used to draw grammar underlines over the Docs canvas):
const origMoveTo = CanvasRenderingContext2D.prototype.moveTo;
CanvasRenderingContext2D.prototype.moveTo = function (x, y) {
  lastPoint.set(this, { x, y });
  origMoveTo.call(this, x, y);
};
// ...lineTo, fillRect, clearRect and stroke are wrapped the same way...

// Acquire a handle on the live document text via the undocumented Docs API:
async function initializeAnnotateAPIGetter(extId) {
  if (typeof self._docs_annotate_getAnnotatedText !== 'function') return;
  const annotated = await self._docs_annotate_getAnnotatedText(extId);
  if (annotated && typeof annotated.getText === 'function')
    getFullTextFn = annotated.getText.bind(annotated);      // whole document
  if (annotated && typeof annotated.getSelection === 'function')
    getSelectionFn = annotated.getSelection.bind(annotated); // current selection
}
03EvidenceFIELD TABLE
What the injected page script can read from an open Google Doc
FieldValueWhy it matters
Full document text
(entire document body)The complete text of the open Google Doc, obtained via getText() and returned in a qb-gdocs-fullText event on request.
Current text selection
["selected sentence"]The text you currently have highlighted, obtained via getSelection() in response to a qb-gdocs-extractSelections event.
Extension-ID page flag
oldceeleldhonbafppcapldpdifcinjiwindow._docs_annotate_canvas_by_ext set to the QuillBot extension id, signalling to Google Docs which extension is annotating the canvas.
04EvidenceCODE COMPARE
The code that does this

Document text returned to the content script via a custom DOM event

What it actually does
// The content script (isolated world) dispatches 'qb-gdocs-getFullText'.
// The injected page-world script answers by reading the whole document
// and dispatching 'qb-gdocs-fullText' with the text as the event detail:
document.addEventListener('qb-gdocs-getFullText', () => {
  const fullText = getDocumentText();           // getText() or KX_kixApp walk
  dispatchCustomEvent(document, 'qb-gdocs-fullText', { text: fullText });
});
05EvidencePLAIN NOTE
Observation

Reading document text is consistent with QuillBot's stated grammar-checking and paraphrasing function, which needs the document contents to operate inside Google Docs. This analysis confirmed the text-extraction capability is installed and exercisable via the page event; it did not observe the extracted Google Docs text being transmitted to a remote server in this run.

+4 more findings not shown

Where it sends data

Destinations our analysis observed QuillBot contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • collector.quillbot.com

    QuillBot sends data to collector.quillbot.com. One other extension we have analysed sends data here.

  • ol-collector.quillbot.com

    QuillBot sends data to ol-collector.quillbot.com. No other extension we have analysed sends data here.

Updated 30 September 2026iidnbdjijdkbmajdffnidomddglmieko