Is MinionLab safe?

Medium risk

MinionLab is medium risk. Code analysis shows MinionLab can receive task messages from gateway.minionlab.ai, decode a base64 JS field, fetch the task URL, and run the decoded script against the response. Confirmed from shipped code, not an observed gateway task.

MinionLabv0.2.8Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

SeverityCRITICAL
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Remote gateway scripts run on fetched page content

Code analysis shows MinionLab can receive task messages from gateway.minionlab.ai, decode a base64 JS field, fetch the task URL, and run the decoded script against the response.

Confirmed from shipped code, not an observed gateway task.

01EvidenceCAUSE EFFECT
What actually happens
You did this

The gateway sends a task that contains a URL and an encoded script.

The behavior depends on a task arriving through the extension gateway channel.

The extension did this

The extension fetches the URL, decodes the script, and runs it on the fetched response.

The parsed result can then be returned to the gateway over the same task channel.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://gateway.minionlab.ai/dispatch
The code parses a JSON response and uses its server value to open a WebSocket task connection.
Headers
Content-Typeapplication/json
03EvidenceFIELD TABLE
Fields used by the remote task workflow
FieldValueWhy it matters
Task URL
https://example.com/accountLets the gateway choose which page response the extension will fetch and process.
Parser script
A base64-encoded script field in the task messageLets the gateway provide code that decides what to extract from the fetched page response.
Fetched page response
<html><body>Account page text</body></html> (illustrative)The script can process the HTML or text returned by the requested page.
Task identifier
task-9f3c2a71Ties each returned result to the specific task that the gateway assigned.
04EvidenceCODE COMPARE
The code that does this

Shipped code path for decoding gateway scripts and executing them on fetched content

What it actually does
Remote task fetcherdeobfuscated/background.js
class Jc {
    constructor(e, u, r, s, i, a, n, c) {
      this.taskid = e, this.controller = new AbortController, this.born = new Date().getTime(), this.timeout = c;
      let l = {
        signal: this.controller.signal,
        method: u,
        headers: s
      };
      i !== "" && u === "POST" && (l.body = i);
      let h = -1;
      this.task = fetch(r, l).then(async d => {
        let y = await d.text();
        console.log("respone is " + y);
        let S = "";
        a.length > 0 && (S = await _t.execute(a, y, this.timeout)), h = d.status;
        let T = self.btoa(self.encodeURIComponent(y));
        if (d.ok) ao(e, S, n ? T : "", h);
        else throw new Wt(zt.NETWORK_ERROR, y)
      }).catch(d => {
        console.error("error " + d.message), Si(e, d.message, d.code || dr, h)
      }).finally(() => {
        console.log("task " + e.toString() + " done")
      })
    }
    cancel() {
      this.controller.abort()
    }
  }
Gateway task message handlerdeobfuscated/background.js
class uo {
    constructor(e, u, r) {
      this.conns = new Map;
      let s = new WebSocket("wss://" + e + "/connect");
      this.timerId = setInterval(i => {
        i.ws && i.ws.send(JSON.stringify({
          type: "ping"
        }))
      }, Yc, this), this.clearTimerId = setInterval(i => {
        let a = new Date().getTime();
        for (const [n, c] of i.conns.entries()) c.born + to < a && (c.cancel(), console.log("task " + n + " expired born at " + c.born.toString() + " now is " + a.toString()))
      }, eo, this), s.onopen = () => {
        console.log("gateway open");
        let i = JSON.stringify({
          type: "register",
          user: u,
          dev: r
        });
        s.send(i), this.ws = s
      }, s.onmessage = i => {
        if (typeof i.data != "string" || i.data == "pong") {
          console.log(i.data);
          return
        }
        let a;
        try {
          console.log("evt.data is " + i.data), a = JSON.parse(i.data), this._handleMessage(a)
        } catch (n) {
          console.log("on message " + n.toString() + " " + i.data), a && a.taskid && typeof a.taskid == "string" && Si(a.taskid, n.toString())
        }
      }, s.onerror = i => {
        console.log("wsocket error " + i), s.close()
      }, s.onclose = () => {
        console.log("wsocket closed"), this.ws = null, no()
      }
    }
    _handleMessage(e) {
      switch (e.type) {
        case "request":
          this._handleRequest(e);
          break;
        case "cancel":
          this._handleCancel(e.taskid);
          break;
        case "show":
          this._handleShow();
        default:
          console.log("unexpepct msg type " + e.type)
      }
    }
    _handleCancel(e) {
      let u = this.conns.get(e);
      this.conns.delete(e) && u.cancel()
    }
    _handleRequest(e) {
      let u = e.taskid && typeof e.taskid == "string" ? e.taskid : "",
        r = e.data && e.data.debug && typeof e.data.debug == "boolean" ? e.data.debug : !1,
        s = e.data.method && typeof e.data.method == "string" ? e.data.method : "GET",
        i = e.data.url && typeof e.data.url == "string" ? e.data.url : "",
        a = e.data.headers && e.data.headers instanceof Object ? e.data.headers : {},
        n = e.data.body && typeof e.data.body == "string" ? e.data.body : "",
        c = e.data.timeout || 15e3,
        l = e.data.script && typeof e.data.script == "string" ? e.data.script : "";
      if (this.conns.size >= Xc) throw new Error("conns limit exceed " + this.conns.size.toString());
      if (this.conns.get(u)) throw new Error("conns duplicated " + u.toString());
      if (l !== "" && (l = self.atob(l)), console.log("script is " + l), n !== "" && (n = self.atob(n)), console.log("body is " + n), i === "") throw new Error("conn without url");
      if (u === "") throw new Error("conn without taskid");
      this.conns.set(u, new Jc(u, s, i, a, n, l, r, c))
    }
    _handleShow() {
      console.log("show tasks");
      let e = JSON.stringify({
        type: "show",
        tasks: Array.from(this.conns.keys())
      });
      this.ws && this.ws.send(e)
    }
    clear() {
      this.clearTimerId && (clearInterval(this.clearTimerId), this.clearTimerId = null), this.timerId && (clearInterval(this.timerId), this.timerId = null), this.ws && (this.ws.close(), this.ws = null), this.conns.clear()
    }
    transferResult(e, u, r, s) {
      if (!this.conns.delete(e)) {
        console.log("transfer result -- deleted already " + e.toString());
        return
      }
      if (!this.ws) return;
      let i = JSON.stringify({
        type: "response",
        taskid: e,
        result: {
          parsed: u,
          html: r,
          rawStatus: s
        }
      });
      console.log("result is " + i), this.ws.send(i)
    }
    transferError(e, u, r, s) {
      if (!this.conns.delete(e)) {
        console.log("transfer error -- deleted already " + e.toString());
        return
      }
      let i = JSON.stringify({
        type: "error",
        taskid: e,
        error: u,
        errorCode: r,
        rawStatus: s
      });
      this.ws && (console.log("transfer error " + i), this.ws.send(i))
    }
  }
Dispatch loop and script executordeobfuscated/background.js
class io {
    constructor(e, u, r) {
      this.user = e, this.dev = u, this.server = null, this.gw = null, this.executor = r, setTimeout(() => {
        this._dispatch()
      }, 0), this.timerId = setInterval(() => {
        this._dispatch()
      }, so)
    }
    async execute(e, u, r = 15e3) {
      let s = "";
      try {
        const i = new Promise((a, n) => setTimeout(() => n(new Wt(zt.PARSE_SCRIPT_TIMEOUT, "parse script timeout")), r));
        if (s = await Promise.race([this.executor(e, u), i]), !s || s === "") throw new Wt(zt.PARSE_SCRIPT_RESULT_EMPTY, "parse script result empty")
      } catch (i) {
        throw i instanceof Wt ? i : new Wt(zt.PARSE_SCRIPT_ERROR, i.message)
      }
      return s
    }
    clearGateway() {
      this.gw && this.gw.clear(), this.gw = null, setTimeout(() => {
        console.log("reconnecting"), !this.gw && this.server && (this.gw = new uo(this.server, this.user, this.dev))
      }, 3e3)
    }
    transferResult(e, u, r, s) {
      this.gw && this.gw.transferResult(e, u, r, s)
    }
    transferError(e, u, r = dr, s = -1) {
      this.gw && this.gw.transferError(e, u, r, s)
    }
    _dispatch() {
      fetch(ro, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          user: this.user,
          dev: this.dev
        })
      }).then(e => e.json()).then(e => {
        if (console.log("server is " + e.server.toString()), e.server) {
          let u = this.server;
          this.server = e.server, u != this.server && this.clearGateway()
        }
      }).catch(e => {
        console.error("fetch error : ", e)
      })
    }
  }
Remote script evaluation callbackdeobfuscated/background.js
a = (n, c) => {
      try {
        console.log("执行任务", n);
        let l = c;
        l = l.replace(/[\u0000-\u001F\u007F]/g, "");
        let h = JSON.stringify(l);
        const d = {
            htmlparser2: Zl,
            DOMParser: di
          },
          y = new me(d, {
            timeout: 1e3
          });
        let S = n + `; extractData(${h})`;
        const T = y.evaluate(S);
        return console.log("执行结果->>", T), T
      } catch (l) {
        return console.error("Error executing code:", l), null
      }
    }
05EvidenceTHIRD PARTY LIST
Remote services in the task workflow
  • gateway.minionlab.ai

    Receives the dispatch request and returns the gateway host used for task assignment.

  • api.minionlab.ai

    Configured service home in the extension constants.

  • app.minionlab.ai

    Configured dashboard URL in the extension constants.

What it can do

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

  • Store data in your browser

    storage

  • Detect when you step away from your computer

    idle

Updated 30 September 2026fgamijdhamopilihagheoalbifagafka