Is CanvasNinja – Tab Detection Blocker for Canvas safe?

Medium risk

CanvasNinja – Tab Detection Blocker for Canvas is medium risk. On a paid account, CanvasNinja injects a script into Canvas quiz pages that makes hasFocus() always report true, freezes visibilityState as visible, and drops any focus, blur, or visibilitychange listener the page registers.

Terabits Technolabv1.1.20Chrome Web Store
45Risk
Who publishes it

Terabits Technolab - 1 other listing from the same operator, none carrying a finding

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

Publisher
Terabits Technolab

Same store account

1 other listing published from this account, 3k+ users between them, none of them carrying a finding.

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI FOUND

CanvasNinja overrides tab-focus and page-visibility APIs during quizzes

On a paid account, CanvasNinja injects a script into Canvas quiz pages that makes hasFocus() always report true, freezes visibilityState as visible, and drops any focus, blur, or visibilitychange listener the page registers.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open a Canvas quiz page and switch to another tab or window while the quiz is open.

Canvas (or an embedded proctoring tool) normally listens for exactly this to flag a possible tab switch.

The extension did this

The page reports itself as still focused and fully visible, and any listener the host page tried to attach for focus, blur, or visibility changes never fires.

document.hasFocus() keeps returning true and the page keeps reporting itself as visible regardless of what you actually do with the tab.

02EvidenceFIELD TABLE
Storage flags background.js checks before activating the override
FieldValueWhy it matters
Extension enabled
enabled: trueThe extension's own on/off toggle in its popup.
Paid account
paid: true, lifetimePaid: falseWhether the account has purchased the paid tier, checked via a Supabase-backed status or a local override flag.
Privacy Guard on
privacyGuardEnabled: trueA feature flag the extension itself sets once it detects you're on a quiz page.
03EvidenceCODE COMPARE
The code that does this

inject.js: focus and visibility override, shipped vs deobfuscated

What it actually does
Deobfuscatedinject.js:1-34
(function (window, document) {
  "use strict";
  var quizContext = window.CanvasNinjaQuizContext;
  if (quizContext && quizContext.parse(window.location.href).kind !== "other") {
    var nativeHasFocus = Document.prototype.hasFocus;
    // Skip if hasFocus is already patched (e.g. by a re-injection).
    if (!nativeHasFocus || !/return true/.test(Function.prototype.toString.call(nativeHasFocus))) {
      var frozenState = {
        visibilityState: "visible",
        hidden: false,
        webkitVisibilityState: "visible",
        webkitHidden: false
      };
      for (var key in frozenState) {
        Object.defineProperty(document, key, { value: frozenState[key], writable: true });
      }
      Document.prototype.hasFocus = function () { return true; };

      var blockedWindowEvents = new Set([
        "focus", "blur", "visibilitychange", "webkitvisibilitychange", "pagehide", "pageshow"
      ]);
      [Window, Document].forEach(function (target) {
        var origAdd = target.prototype.addEventListener;
        var origRemove = target.prototype.removeEventListener;
        target.prototype.addEventListener = function (type, listener, opts) {
          if (!blockedWindowEvents.has(type)) origAdd.call(this, type, listener, opts);
        };
        target.prototype.removeEventListener = function (type, listener, opts) {
          if (!blockedWindowEvents.has(type)) origRemove.call(this, type, listener, opts);
        };
      });

      var blockedElementEvents = new Set([
        "focus", "focusin", "focusout", "blur",
        "visibilitychange", "webkitvisibilitychange", "mozvisibilitychange", "msvisibilitychange"
      ]);
      var origElementAdd = Element.prototype.addEventListener;
      Element.prototype._addEventListener = origElementAdd;
      Element.prototype.addEventListener = function (type, listener, opts) {
        if (this && !blockedElementEvents.has(type)) this._addEventListener(type, listener, opts);
      };
      window.addEventListener = document.addEventListener = Element.prototype.addEventListener;
      window._addEventListener = document._addEventListener = origElementAdd;
    }
  }
})(window, document);
04EvidenceCODE COMPARE
The code that does this

background.js: MAIN-world registration, shipped vs deobfuscated

What it actually does
Deobfuscatedbackground.js:233-274
var activatePrivacyGuard = async function () {
  try {
    await config.load();
    var flags = await new Promise(function (resolve) {
      chrome.storage.local.get(
        { enabled: true, paid: false, lifetimePaid: false, privacyGuardEnabled: false },
        resolve
      );
    });
    var overridePaid = config.isPaidOverride === true;
    var isPaid = overridePaid || flags.paid || flags.lifetimePaid;
    var guardOn = overridePaid || flags.privacyGuardEnabled;

    if (!(flags.enabled && isPaid && guardOn)) {
      await chrome.scripting.unregisterContentScripts({ ids: ["main"] }).catch(function () {});
      console.log("Privacy Guard OFF or unpaid \u2192 inject.js unregistered");
      return;
    }

    await chrome.scripting.unregisterContentScripts({ ids: ["main"] }).catch(function (e) {
      console.warn("Unregister error", e);
    });

    var already = (await chrome.scripting.getRegisteredContentScripts())
      .some(function (s) { return s.id === "main"; });
    if (already) {
      console.log("main already present; skipping");
      return;
    }
    try {
      await chrome.scripting.registerContentScripts([{
        id: "main",
        js: ["quiz-context.js", "inject.js"],
        matches: ["*://*/courses/*"],
        world: "MAIN",
        runAt: "document_start",
        allFrames: true
      }]);
      console.log("main script registered");
    } catch (e) {
      if (!e.message.includes("Duplicate script ID")) throw e;
      console.log("main was already registered \u2014 skipping");
    }
  } catch (e) {
    console.error("Activate error:", e);
  }
};
05EvidenceARTIFACT
Check if you're affected

Run in the page console on a Canvas quiz page to check whether hasFocus, visibilityState, and visibility-related event listeners have been overridden.

RequiresChrome DevTools console access on the quiz page
check-focus-override.js · js
// Run in the DevTools console on a Canvas quiz page.
// A stock browser reports native code for hasFocus and fires
// visibilitychange on tab switch. If CanvasNinja's Privacy Guard is
// active, neither of those will be true.
(function check() {
  var isNative = /\[native code\]/.test(Document.prototype.hasFocus.toString());
  console.log('hasFocus is native:', isNative);

  var fired = false;
  document.addEventListener('visibilitychange', function () { fired = true; });
  console.log('Switch tabs now, then re-run: window.__cnVisibilityFired');
  window.__cnVisibilityFired = function () { return fired; };

  console.log('document.hidden:', document.hidden, '| visibilityState:', document.visibilityState);
})();
How to run it
  1. 1
    Open a Canvas quiz page.
  2. 2
    Open DevTools console and paste the script.
  3. 3
    Switch to another tab for a few seconds, switch back.
  4. 4
    Run window.__cnVisibilityFired() and check document.visibilityState again.
06EvidencePLAIN NOTE
Observation

Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.

Updated 30 September 2026ailcnoigddfddkfnppjcgjijaoehiime