Is SmartSolve - AI Homework Tutor safe?

Medium risk

SmartSolve captures screenshots and selected text from any tab and transmits them to api.smartsolve.ai, while suppressing browser events to conceal itself from exam platforms.

When triggered by a hotkey or right-click menu, the extension captures either a visible screenshot or selected text from the active tab and forwards it to its AI answer service at api.smartsolve.ai. It also injects a message listener into every page that relays arbitrary text to the same endpoint without validating the message origin, meaning any page can silently submit content to the AI on the user's behalf. Additionally, the extension blocks visibilitychange, blur, and focus events on all pages to prevent online exam platforms from detecting its presence.

SmartSolvev1.0.11Chrome Web Store
53Risk
Who publishes it

NIKNEA, LLC - 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
SmartSolve
Declared legal entity
NIKNEA, LLC
Registered address
7660H Fay Ave Ste 378, LA JOLLA, CA 92037, US
Registered contact
NIKNEA, LLC

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

Publishers can request a review.

Findings

Visible tab screenshot sent after SmartSolve snapshot

MediumunwantedCWE-359Dynamic sandbox

Choosing SmartSolve Snapshot, or Shift+S+1, captures the visible tab as a JPEG data URL, feeding it into the chat flow.

What actually happens

You did this

You ask SmartSolve to take a snapshot of the current page.

The trigger is either Shift+S+1 or the SmartSolve Snapshot context-menu item.

The extension did this

The extension captures the visible tab and sends the image into an answer request.

The captured value is a JPEG data URL that is added as image input.

Data placed into the answer request

  • Visible tab image
    data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD9k= (illustrative)

    The image can include whatever text, account information, or page content is visible when you trigger the snapshot.

  • SmartSolve session token
    Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature (illustrative)

    The request is tied to the signed-in SmartSolve account used by the extension.

Captured request

POSThttps://api.smartsolve.ai/api/answer

Server-sent event stream used for SmartSolve answer generation.

Headers
content-type
application/json
Authorization
Z1.getSnapshot().token

The code that does this

Snapshot capture and answer submission path

Readable version

Readable background listener captures and forwards the snapshot

src/pages/background/index.js
chrome.runtime.onMessage.addListener((t, e, n) => {  if (typeof t == "object" && t.type === "tab-capture") {    if (typeof t.data == "string") x("/answers", {      type: "init-chat",      dataType: "image",      data: t.data    });    else if (t.start === !0) chrome.tabs.query({      active: !0,      currentWindow: !0    }).then(s => {      _(s[0])    }), n(!0);    else if (e.tab) return chrome.tabs.captureVisibleTab(e.tab.windowId).then(s => {      n(s)    }), !0  }});

Readable snapshot helper injects the capture UI into the tab

src/pages/background/index.js
function _(t, e) {  !t.id || chrome.scripting.executeScript({    target: {      tabId: t.id,      frameIds: e != null && e.frameId ? [e.frameId] : void 0    },    func: A,    args: [t.id + 261]  })}

Readable menu and keyboard triggers call the snapshot path

src/pages/background/index.js
function I() {  chrome.contextMenus.create({    contexts: ["selection"],    id: "text-select",    title: "SmartSolve It!"  }, () => {    chrome.runtime.lastError  }), chrome.contextMenus.create({    contexts: ["page"],    id: "snapshot-select",    title: "SmartSolve Snapshot"  }, () => {    chrome.runtime.lastError  })}g.addEventListener("change", () => {  var t, e, n, s;  ((t = g.currentSettings) == null ? void 0 : t.rightClick) === !1 || !((e = g.currentAuth) != null && e.token) ? chrome.contextMenus.removeAll() : (((n = g.currentSettings) == null ? void 0 : n.rightClick) === !0 || ((s = g.currentAuth) == null ? void 0 : s.token)) && I()});chrome.runtime.onInstalled.addListener(async () => {  var t, e;  ((t = g.currentSettings) == null ? void 0 : t.rightClick) !== !1 && ((e = g.currentAuth) == null ? void 0 : e.token) && I()});chrome.contextMenus.onClicked.addListener(async (t, e) => {  !e || (t.menuItemId === "text-select" ? q(e, t) : t.menuItemId === "snapshot-select" && _(e, t))});const j = {    snapshot: ["ShiftLeft", "KeyS", "Digit1"],    highlightedText: ["ShiftLeft", "KeyS", "Digit2"]  },

Readable popup sends non-client chat messages to the answer API

src/pages/popup/index.js
M2 = {    BASE_URL: "https://smartsolve.ai/api",    AI_BASE_URL: "https://api.smartsolve.ai/api"  },  e0 = () => t3({    mutationFn: ({      message: s,      chatId: o,      visual: n,      forceVisual: x,      clearChat: H    }) => new Promise(L => {      const m = j2(),        g = {          id: m,          message: F3(s),          type: "user",          streaming: !1,          clientOnly: !1,          isLoading: !n && s.startsWith("data:image/jpeg;base64,"),          visual: !!n,          forceVisual: !!n && !!x        };      A1.addMessage(g, o);      const f = A1.getChat(o);      H && (f.messages = f.messages.filter(D => D.id !== m ? (A1.removeMessage(D.id, o), !1) : !0));      const k = j2(),        u = new J6.SSE(`${M2.AI_BASE_URL}/answer`, {          payload: JSON.stringify({            chat: f.messages.filter(D => !D.clientOnly),            id: o          }),          headers: {            "content-type": "application/json",            Authorization: Z1.getSnapshot().token          },          start: !1        });      A1.addMessage({        id: k,        message: "",        type: "ss",        clientOnly: !1,        streaming: !0,        isLoading: !0      }, o), u.addEventListener("ocr_result", D => {        const V = JSON.parse(D.data).message;        A1.addMessage({          ...g,          message: F3(V),          isLoading: !1        }, o)      });      let y = "";      u.addEventListener("work", async D => {        var G;        const V = JSON.parse(D.data).message,          Q = (G = A1.getChat(o)) == null ? void 0 : G.messages.find(v => v.id === k);        !Q || (Q.isLoading && (Q.isLoading = !1), Q.message += V, y += V, A1.addMessage(Q, o))      });      let M = "";      const d = j2();      u.addEventListener("answer", async D => {        const V = JSON.parse(D.data).message;        M += V, M.length > 200 && A1.addMessage({          id: d,          message: "",          type: "ss",          clientOnly: !0,          streaming: !0,          isLoading: !0        }, o)      }), u.addEventListener("tickets_update", D => {        const V = JSON.parse(D.data);        typeof V.message == "number" && (w2.saveTickets(V.message), w2.setShowTicketBanner(!0))      }), u.addEventListener("ocr_error", () => {        y = "error", A1.removeMessage(m, o), A1.addMessage({          id: k,          message: x9,          clientOnly: !0,          isLoading: !1,          type: "ss",          streaming: !0        }, o)      }), u.addEventListener("change_type", D => {        JSON.parse(D.data).blocked && b3.setVisionBlocked(Date.now()), y = "error"      }), u.addEventListener("answer_error", () => {        y = "error", A1.removeMessage(m, o), A1.addMessage({          id: k,          message: h9,          clientOnly: !0,          isLoading: !1,          type: "ss",          streaming: !0        }, o)      }), u.addEventListener("trial_limit", () => {        y = "error", A1.removeMessage(m, o), Z1.setHitTrialLimit(!0)      }), u.addEventListener("custom_error", D => {        const V = JSON.parse(D.data);        y = "error", A1.addMessage({          id: k,          message: V.message,          streaming: !!V.s,          clientOnly: V.c !== !1,          type: "ss",          isLoading: !1        }, o)      }), u.addEventListener("error", D => {        D.data === "Unauthorized" && Z1.clear(), y = "error", A1.removeMessage(m, o), A1.addMessage({          id: k,          message: L9,          clientOnly: !0,          isLoading: !1,          type: "ss",          streaming: !1        }, o)      }), u.addEventListener("readystatechange", D => {        var V;        if (D.readyState === 2) {          const Q = y.toLowerCase().replace(/\s\s+/g, " "),            G = M.toLowerCase().replace(/\s\s+/g, " ");          !y && M.length < 200 ? A1.addMessage({            id: k,            message: m9,            clientOnly: !0,            isLoading: !1,            type: "ss",            streaming: !0          }, o) : M.length < 200 ? A1.setAnswer(F3(M), o) : Q.includes(G.toLowerCase()) ? G.includes(Q.toLowerCase()) && A1.removeMessage(d, o) : A1.addMessage({            id: d,            message: F3(M),            type: "ss",            clientOnly: !!y,            streaming: !0,            isLoading: !1          }, o), y !== "error" && A1.lockAnswer(o);          const v = (V = A1.getChat(o)) == null ? void 0 : V.messages.find(P => P.id === k);          v && (v.message = F3(v.message), A1.addMessage(v, o)), setTimeout(() => {            A1.stopStreaming(d, o), A1.stopStreaming(k, o)          }, 100), L(1)        }      }), u.stream()    })  }),

Network destination for the captured image

    • api.smartsolve.ai

    Receives the SmartSolve answer request that includes the image chat message.

Page focus and visibility events blocked on load

MediumunwantedCWE-506Dynamic sandbox

On every page load, SmartSolve injects code into all frames before page scripts finish registering.

What actually happens

You did this

You open or reload a page while the extension is installed.

The behavior is tied to the tab loading state, not to a menu item.

The extension did this

The extension injects code that prevents page focus and visibility events from reaching website handlers.

The code runs immediately in every frame in the page context.

Browser signals affected by the injected listeners

  • Tab visibility changes
    visibilitychange

    A site may not receive the normal signal when your tab becomes visible or not visible.

  • WebKit visibility changes
    webkitvisibilitychange

    A site using the browser-specific visibility event may not receive that signal either.

  • Top-window blur
    blur on the top window

    A site may not receive the normal signal when the page window loses focus.

  • Top-window focus
    focus on the top window

    A site may not receive the normal signal when the page window regains focus.

The code that does this

Immediate page-context event interception

Readable version

Readable service worker injects capture-phase event blockers on page load

src/pages/background/index.js
chrome.tabs.onUpdated.addListener((t, e, n) => {  e.status === "loading" && chrome.scripting.executeScript({    target: {      tabId: t,      allFrames: !0    },    func: () => {      window.addEventListener("visibilitychange", s => {        s.stopImmediatePropagation()      }, {        capture: !0      }), window.addEventListener("webkitvisibilitychange", s => {        s.stopImmediatePropagation()      }, {        capture: !0      }), window.addEventListener("blur", function(s) {        if (this.window === s.target) return s.stopImmediatePropagation(), !1      }, {        capture: !0      }), window.addEventListener("focus", function(s) {        this.window === s.target && s.stopImmediatePropagation()      }, {        capture: !0      })    },    injectImmediately: !0,    world: "MAIN"  })});

Selected page text sent to SmartSolve answer API

MediumunwantedCWE-359Dynamic sandbox

Highlighting text and pressing SmartSolve It (or Shift+S+2) sends the page selection to SmartSolve.

What actually happens

You did this

You select text and invoke SmartSolve on the page.

The trigger is Shift+S+2 or the SmartSolve It context-menu item.

The extension did this

The extension reads the highlighted text and sends it as a SmartSolve answer request.

The text is sent as the user message in the chat payload.

Data placed into the answer request

  • Selected page text
    Solve for x in 2x + 3 = 11 (illustrative)

    The highlighted text can include page content you selected before invoking SmartSolve.

  • SmartSolve session token
    Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature (illustrative)

    The request is tied to the signed-in SmartSolve account used by the extension.

Captured request

POSThttps://api.smartsolve.ai/api/answer

Server-sent event stream used for SmartSolve answer generation.

Headers
content-type
application/json
Authorization
Z1.getSnapshot().token

The code that does this

Selected-text capture and answer submission path

Readable version

Readable text-capture listener reads window selection

src/pages/background/index.js
chrome.runtime.onMessage.addListener((t, e) => {  typeof t == "object" && t.type === "text-capture" && q(e.tab)});async function q(t, e) {  var s;  if (!t.id) return;  const n = await chrome.scripting.executeScript({    target: {      tabId: t.id,      frameIds: e != null && e.frameId ? [e.frameId] : void 0    },    func: () => {      var a;      return (a = window.getSelection()) == null ? void 0 : a.toString()    }  }).catch(() => {});  (typeof n < "u" || (e == null ? void 0 : e.selectionText)) && x("/answers", {    type: "init-chat",    dataType: "text",    data: ((s = n == null ? void 0 : n[0]) == null ? void 0 : s.result) || (e == null ? void 0 : e.selectionText)  })}

Readable menu and keyboard triggers call the text path

src/pages/background/index.js
function I() {  chrome.contextMenus.create({    contexts: ["selection"],    id: "text-select",    title: "SmartSolve It!"  }, () => {    chrome.runtime.lastError  }), chrome.contextMenus.create({    contexts: ["page"],    id: "snapshot-select",    title: "SmartSolve Snapshot"  }, () => {    chrome.runtime.lastError  })}g.addEventListener("change", () => {  var t, e, n, s;  ((t = g.currentSettings) == null ? void 0 : t.rightClick) === !1 || !((e = g.currentAuth) != null && e.token) ? chrome.contextMenus.removeAll() : (((n = g.currentSettings) == null ? void 0 : n.rightClick) === !0 || ((s = g.currentAuth) == null ? void 0 : s.token)) && I()});chrome.runtime.onInstalled.addListener(async () => {  var t, e;  ((t = g.currentSettings) == null ? void 0 : t.rightClick) !== !1 && ((e = g.currentAuth) == null ? void 0 : e.token) && I()});chrome.contextMenus.onClicked.addListener(async (t, e) => {  !e || (t.menuItemId === "text-select" ? q(e, t) : t.menuItemId === "snapshot-select" && _(e, t))});const j = {    snapshot: ["ShiftLeft", "KeyS", "Digit1"],    highlightedText: ["ShiftLeft", "KeyS", "Digit2"]  },

Readable hotkey listener sends text-capture for Shift+S+2

src/pages/background/index.js
chrome.tabs.onUpdated.addListener(async (t, e, n) => {  var s, a;  if (e.status === "loading") {    const C = (a = (s = g.currentSettings) == null ? void 0 : s.keys) != null ? a : j;    chrome.scripting.executeScript({      target: {        tabId: t,        allFrames: !0      },      func: (m, c, o) => {        let i = Math.sin(o) * 1e4;        if (i = (i - Math.floor(i)).toString().replace(".", ""), window[`_s${i}`]) return;        window[`_s${i}`] = !0;        function d(p) {          if (Array.isArray(p)) return p.map(h => {            var y;            return (y = c[h]) != null ? y : h          }).sort().join()        }        let r = d(m.highlightedText),          u = d(m.snapshot);        chrome.runtime.onMessage.addListener(p => {          var h, y;          typeof p == "object" && p.type === "hotkey-update" && ((h = p.keys) != null && h.highlightedText && (r = d(p.keys.highlightedText)), (y = p.keys) != null && y.snapshot && (u = d(p.keys.snapshot)))        });        let l = [];        function w() {          var p, h;          if (l.length === 2 || l.length === 3) {            const y = d(l);            y === u ? (chrome.runtime.sendMessage({              type: "tab-capture",              start: !0            }), l = []) : y === r && ((h = (p = window.getSelection()) == null ? void 0 : p.toString()) == null ? void 0 : h.length) > 0 && (chrome.runtime.sendMessage({              type: "text-capture"            }), l = [])          }        }        window.addEventListener("blur", () => {          l = []        }), window.addEventListener("focus", () => {          l = []        }), window.addEventListener("keydown", p => {          var h, y;          l.includes((h = c[p.code]) != null ? h : p.code) || l.push((y = c[p.code]) != null ? y : p.code), w()        }), window.addEventListener("keyup", p => {          var y;          const h = l.indexOf((y = c[p.code]) != null ? y : p.code);          h !== -1 && l.splice(h, 1)        })      },      args: [C, $, n.id + 263]    }).then(() => {      v.push(t)    })  }});

Readable popup sends non-client chat messages to the answer API

src/pages/popup/index.js
M2 = {    BASE_URL: "https://smartsolve.ai/api",    AI_BASE_URL: "https://api.smartsolve.ai/api"  },  e0 = () => t3({    mutationFn: ({      message: s,      chatId: o,      visual: n,      forceVisual: x,      clearChat: H    }) => new Promise(L => {      const m = j2(),        g = {          id: m,          message: F3(s),          type: "user",          streaming: !1,          clientOnly: !1,          isLoading: !n && s.startsWith("data:image/jpeg;base64,"),          visual: !!n,          forceVisual: !!n && !!x        };      A1.addMessage(g, o);      const f = A1.getChat(o);      H && (f.messages = f.messages.filter(D => D.id !== m ? (A1.removeMessage(D.id, o), !1) : !0));      const k = j2(),        u = new J6.SSE(`${M2.AI_BASE_URL}/answer`, {          payload: JSON.stringify({            chat: f.messages.filter(D => !D.clientOnly),            id: o          }),          headers: {            "content-type": "application/json",            Authorization: Z1.getSnapshot().token          },          start: !1        });      A1.addMessage({        id: k,        message: "",        type: "ss",        clientOnly: !1,        streaming: !0,        isLoading: !0      }, o), u.addEventListener("ocr_result", D => {        const V = JSON.parse(D.data).message;        A1.addMessage({          ...g,          message: F3(V),          isLoading: !1        }, o)      });      let y = "";      u.addEventListener("work", async D => {        var G;        const V = JSON.parse(D.data).message,          Q = (G = A1.getChat(o)) == null ? void 0 : G.messages.find(v => v.id === k);        !Q || (Q.isLoading && (Q.isLoading = !1), Q.message += V, y += V, A1.addMessage(Q, o))      });      let M = "";      const d = j2();      u.addEventListener("answer", async D => {        const V = JSON.parse(D.data).message;        M += V, M.length > 200 && A1.addMessage({          id: d,          message: "",          type: "ss",          clientOnly: !0,          streaming: !0,          isLoading: !0        }, o)      }), u.addEventListener("tickets_update", D => {        const V = JSON.parse(D.data);        typeof V.message == "number" && (w2.saveTickets(V.message), w2.setShowTicketBanner(!0))      }), u.addEventListener("ocr_error", () => {        y = "error", A1.removeMessage(m, o), A1.addMessage({          id: k,          message: x9,          clientOnly: !0,          isLoading: !1,          type: "ss",          streaming: !0        }, o)      }), u.addEventListener("change_type", D => {        JSON.parse(D.data).blocked && b3.setVisionBlocked(Date.now()), y = "error"      }), u.addEventListener("answer_error", () => {        y = "error", A1.removeMessage(m, o), A1.addMessage({          id: k,          message: h9,          clientOnly: !0,          isLoading: !1,          type: "ss",          streaming: !0        }, o)      }), u.addEventListener("trial_limit", () => {        y = "error", A1.removeMessage(m, o), Z1.setHitTrialLimit(!0)      }), u.addEventListener("custom_error", D => {        const V = JSON.parse(D.data);        y = "error", A1.addMessage({          id: k,          message: V.message,          streaming: !!V.s,          clientOnly: V.c !== !1,          type: "ss",          isLoading: !1        }, o)      }), u.addEventListener("error", D => {        D.data === "Unauthorized" && Z1.clear(), y = "error", A1.removeMessage(m, o), A1.addMessage({          id: k,          message: L9,          clientOnly: !0,          isLoading: !1,          type: "ss",          streaming: !1        }, o)      }), u.addEventListener("readystatechange", D => {        var V;        if (D.readyState === 2) {          const Q = y.toLowerCase().replace(/\s\s+/g, " "),            G = M.toLowerCase().replace(/\s\s+/g, " ");          !y && M.length < 200 ? A1.addMessage({            id: k,            message: m9,            clientOnly: !0,            isLoading: !1,            type: "ss",            streaming: !0          }, o) : M.length < 200 ? A1.setAnswer(F3(M), o) : Q.includes(G.toLowerCase()) ? G.includes(Q.toLowerCase()) && A1.removeMessage(d, o) : A1.addMessage({            id: d,            message: F3(M),            type: "ss",            clientOnly: !!y,            streaming: !0,            isLoading: !1          }, o), y !== "error" && A1.lockAnswer(o);          const v = (V = A1.getChat(o)) == null ? void 0 : V.messages.find(P => P.id === k);          v && (v.message = F3(v.message), A1.addMessage(v, o)), setTimeout(() => {            A1.stopStreaming(d, o), A1.stopStreaming(k, o)          }, 100), L(1)        }      }), u.stream()    })  }),

Network destination for the selected text

    • api.smartsolve.ai

    Receives the SmartSolve answer request that includes the selected text chat message.

What it can do

Permissions this extension asks for, as declared in version 1.0.11. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every site you visit

    <all_urls>

  • Store data in your browser

    storage

  • Run its own code inside the pages you visit

    scripting

  • Add items to the right-click menu

    contextMenus

Where it sends data

Destinations our analysis observed SmartSolve - AI Homework Tutor 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.

  • api.smartsolve.ai

    SmartSolve - AI Homework Tutor sends data to api.smartsolve.ai. No other extension we have analyzed sends data here.

Updated 30 September 2026lamlihfgchoafpnomkjblficpfeonace