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
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Visible tab screenshot sent after SmartSolve snapshot

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

The popup opens a server-sent-events POST to api.smartsolve.ai/api/answer, carrying the signed-in authorization token.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Data placed into the answer request
FieldValueWhy it matters
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.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.smartsolve.ai/api/answer
Server-sent event stream used for SmartSolve answer generation.
Headers
content-typeapplication/json
AuthorizationZ1.getSnapshot().token
04EvidenceCODE COMPARE
The code that does this

Snapshot capture and answer submission path

What it actually does
Readable background listener captures and forwards the snapshotsrc/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 tabsrc/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 pathsrc/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 APIsrc/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()
    })
  }),
05EvidenceTHIRD PARTY LIST
Network destination for the captured image
  • api.smartsolve.ai

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

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI SANDBOX

Page focus and visibility events blocked on load

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

That code blocks visibility and focus events from reaching site handlers, so sites may miss normal tab-focus signals.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Browser signals affected by the injected listeners
FieldValueWhy it matters
Tab visibility changes
visibilitychangeA site may not receive the normal signal when your tab becomes visible or not visible.
WebKit visibility changes
webkitvisibilitychangeA site using the browser-specific visibility event may not receive that signal either.
Top-window blur
blur on the top windowA site may not receive the normal signal when the page window loses focus.
Top-window focus
focus on the top windowA site may not receive the normal signal when the page window regains focus.
03EvidenceCODE COMPARE
The code that does this

Immediate page-context event interception

What it actually does
Readable service worker injects capture-phase event blockers on page loadsrc/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"
  })
});
SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Selected page text sent to SmartSolve answer API

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

The extension reads the selection and POSTs it via SSE to https://api.smartsolve.ai/api/answer for the signed-in session.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Data placed into the answer request
FieldValueWhy it matters
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.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.smartsolve.ai/api/answer
Server-sent event stream used for SmartSolve answer generation.
Headers
content-typeapplication/json
AuthorizationZ1.getSnapshot().token
04EvidenceCODE COMPARE
The code that does this

Selected-text capture and answer submission path

What it actually does
Readable text-capture listener reads window selectionsrc/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 pathsrc/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+2src/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 APIsrc/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()
    })
  }),
05EvidenceTHIRD PARTY LIST
Network destination for the selected text
  • api.smartsolve.ai

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

Data recipients

api.smartsolve.ai
Updated 17 September 2026lamlihfgchoafpnomkjblficpfeonace