Is CanvasNinja – Tab Detection Blocker for Canvas safe?
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.
Who publishes itTerabits Technolab - 1 other listing from the same operator, none carrying a finding
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.
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
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.
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 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.
| Field | Value | Why it matters | |
|---|---|---|---|
Extension enabled | enabled: true | The extension's own on/off toggle in its popup. | |
Paid account | paid: true, lifetimePaid: false | Whether the account has purchased the paid tier, checked via a Supabase-backed status or a local override flag. | |
Privacy Guard on | privacyGuardEnabled: true | A feature flag the extension itself sets once it detects you're on a quiz page. |
inject.js: focus and visibility override, shipped vs deobfuscated
(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);background.js: MAIN-world registration, shipped vs deobfuscated
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);
}
};Run in the page console on a Canvas quiz page to check whether hasFocus, visibilityState, and visibility-related event listeners have been overridden.
// 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);
})();
- 1Open a Canvas quiz page.
- 2Open DevTools console and paste the script.
- 3Switch to another tab for a few seconds, switch back.
- 4Run window.__cnVisibilityFired() and check document.visibilityState again.
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.