Is Primus safe?

Medium risk

Primus is medium risk. Starting a Primus attestation makes the extension register request listeners on the tab, record matched headers, cookies, bodies, and place them into proof params sent to the offscreen engine. An earlier test saw only padolabs.org traffic.…

Primus Labsv0.4.13Chrome Web Store
45Risk
Who publishes it

Primus Labs - 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
Primus Labs

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

Publishers can request a review.

Findings

Attestation flow copies headers and cookies

HighunwantedCWE-200Dynamic sandbox

Starting a Primus attestation makes the extension register request listeners on the tab, record matched headers, cookies, bodies, and place them into proof params sent to the offscreen engine.

What actually happens

You did this

You start a Primus attestation for a data-source site.

The extension opens or uses a tab for that site as part of the proof flow.

The extension did this

The extension records matched requests from that tab and prepares them as proof inputs.

Headers, cookies, and POST bodies are copied when the active template says the request must be captured.

Fields the attestation path can copy from matched requests

  • Session cookies
    Cookie: luma.auth-session-key=sess_7f4b3d2a91c845bb; csrf=bd6d2f3c1a994d1e (illustrative)

    This can identify your signed-in session for the site used in the attestation.

  • Authorization header
    Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDAxIn0.Ji6Q0l3YbQ4L9wVmzK3qYsB5cR7n2qH8pW1dAeT0sYg (illustrative)

    This can carry a bearer token or similar value that proves your account is signed in.

  • Request body
    POST body: {"accountType":"SPOT","timestamp":1720771200000} (illustrative)

    This can include the submitted parameters for the account or API request being proved.

  • Matched request URL
    https://api.binance.com/api/v3/account?timestamp=1720771200000 (illustrative)

    This shows which account or API endpoint the proof flow matched during your attestation.

  • Anti-CSRF header
    csrf-token: ajax:734920156421 (illustrative)

    This can link the copied request to the signed-in browser session for that site.

The code that does this

The shipped service worker captures headers and bodies before building proof parameters

Readable version

Request listeners registered for all URLs

background.bundle.js
a.onBeforeSendHeadersFn = d, a.onBeforeRequestFn = f, chrome.webRequest.onBeforeSendHeaders.addListener(d, {  urls: ["<all_urls>"],  types: ["xmlhttprequest", "main_frame"]}, ["requestHeaders", "extraHeaders"]), chrome.webRequest.onBeforeRequest.addListener(f, {  urls: ["<all_urls>"],  types: ["xmlhttprequest", "main_frame"]}, ["requestBody"])

Matched request headers are stored

background.bundle.js
console.log("captured request", f, JSON.stringify(e));const a = {  headers: s,  method: r,  url: f,  requestId: n,  templateRequestUrl: p,  type: e.type};i && (a.queryString = i), c(n, a), await lc()

Request bodies are stored when present

background.bundle.js
if (r?.raw?.[0]?.bytes) {  const a = new Uint8Array(r.raw[0].bytes),    t = (new TextDecoder).decode(a);  c(n, {    ...e,    body: JSON.parse(t)  })}r?.formData && c(n, {  ...e,  body: r.formData,  isFormData: !0})

Stored requests become algorithm parameters

background.bundle.js
Object.assign(p, {  reqType: "web",  host: S,  schemaType: b,  requests: m,  responses: T,  uiTemplate: x,  templateId: u,  calculations: s,  PADOSERVERURL: X,  padoExtensionVersion: e}), await ka(p, t), qe(p, t), Je(p, t), c.formatAlgorithmParams = p, console.log("formatAlgorithmParams", p, t)

Parameters are sent to the offscreen proof engine

background.bundle.js
const a = Object.assign({    isUserClick: "true"  }, s.formatAlgorithmParams),  c = await D(),  t = {    ...a,    clientType: c?.clientType || ""  };await _({  activeRequestAttestation: JSON.stringify(a)}), console.log("pageDecode-algorithmParams", a), chrome.runtime.sendMessage({  type: "algorithm",  method: "getAttestation",  params: t})

The WASM bridge calls callAlgorithm

offscreen.js
_call(e, t = {}, i) {  const s = {      method: e,      version: i || CLIENT_VERSION,      params: t    },    n = JSON.stringify(s);  return Module.cwrap("callAlgorithm", "string", ["string"])(n)}getAttestation(e) {  const t = getEffectiveVersion(e?.clientType);  if (console.log("getAttestation AlgorithmInited=", this.initialized), !this.initialized) return JSON.stringify({    content: null,    retcode: "2",    retdesc: "Algorithm not initialized"  });  const i = this._call("getAttestation", e, t);  return console.log("getAttestation typeof res", typeof i, "res", i), i}

PADO hosts used by the proof workflow

    • api.padolabs.org

    Production API base used for identity, event reporting, logs, and runtime configuration.

    • api2.padolabs.org

    Production WebSocket host hardcoded for algorithm-proxy and algoproxy endpoints.

Captured request

POSThttps://api.padolabs.org/public/event/report

Observed during dynamic analysis along with GET https://api.padolabs.org/public/pado/identity; no algoproxy request body was recorded in that run.

Primus replays signed-in exchange API requests

MediumunwantedCWE-200Dynamic sandbox

Starting a Primus proof flow lets the extension retain headers from the data-source tab and replay matching exchange API requests with credentials included.

What actually happens

You did this

You start a Primus proof flow that uses a signed-in exchange account.

The start handler marks the flow as a user click and sends the proof parameters to the attestation worker.

The extension did this

The extension can replay a matching private exchange API request with retained headers and browser credentials included.

The replayed response is used to validate account data before the proof is generated.

Fields used in the replayed exchange request

  • Exchange session cookies
    Cookie: Binance session cookies redacted

    Keeps the replayed request tied to your signed-in exchange session.

  • Captured request headers
    User-Agent: Mozilla/5.0; Accept: application/json

    Preserves the same browser request context that the exchange page used.

  • Private account endpoint
    https://www.binance.com/bapi/asset/v3/private/asset-service/asset/get-user-asset

    Points the replay at an account API that returns data about your exchange profile or assets.

  • Proof-flow context
    activeRequestAttestation with isUserClick true

    Links the replayed exchange response to the proof request that is being generated.

Captured request

POSThttps://www.binance.com/bapi/asset/v3/private/asset-service/asset/get-user-asset

Code-level verification confirmed the replay path for private Binance account APIs; no request body was recorded in the available evidence.

Headers
Cookie
redacted active exchange session cookies

The code that does this

Replay helper keeps headers and browser credentials

Readable version

Readable fetch option builder

background.bundle.js
function ge({  url: e,  method: a,  body: c,  header: t,  isFormData: d}) {  return {    url: e,    method: a,    body: c,    header: t || {},    isFormData: d  }}function ye({  url: e,  method: a,  body: c,  header: t,  isFormData: d}) {  const f = {    method: "GET",    headers: {      "Content-Type": "application/json"    },    credentials: "include",    ...{      method: a,      body: c,      headers: t    }  };  ["POST", "PUT", "PATCH"].includes(f.method.toUpperCase()) && f.body && (i(f.body) || (f.body = JSON.stringify(f.body)));  const b = "application/x-www-form-urlencoded";  if (f.body && t["content-type"]?.includes(b) || t["Content-Type"]?.includes(b) && d) {    let e = i(f.body) ? JSON.parse(f.body) : f.body;    f.body = (r = e, Object.keys(r).map((e => encodeURIComponent(e) + "=" + encodeURIComponent(r[e]))).join("&"))  }  var r;  return {    finalOptions: f,    url: e  }}async function ve(e, a) {  const c = await fetch(a, e);  if (!c.ok) throw new Error(`HTTP error! status: ${c.status}`);  const t = c.headers.get("Content-Type") || "";  let d;  return d = t.includes("application/json") || t.includes("+json") ? await c.json() : t.includes("text/") ? await c.text() : t.includes("application/octet-stream") ? await c.blob() : await c.text(), {    data: d,    contentType: t,    finalUrl: c.url  }}

Readable replay call using stored headers

background.bundle.js
const a = await Se({  ...f[e],  header: f[e].headers,  url: I,  body: E});a && (w = a.data, A = a.contentType, T = a.finalUrl)

The code that does this

The extension stores matching request headers before replay

Readable version
const {  requests: d} = t, {  url: f,  requestHeaders: b,  method: r,  requestId: n} = e;let i = "";const s = b.reduce(((e, a) => (e[a.name] = a.value, e)), {});let p = "";if (d.some((e => {    if (!1 === e.needCapture) return !1;    if (e.queryParams?.[0]) {      const a = f.split("?")[0];      if (e.url === a) {        const a = x(f, e.queryParams);        a && (i = a)      }    }    const a = u({      requestUrl: f,      requiredUrl: e.url,      urlType: e.urlType,      queryParams: e.queryParams    });    return a && (p = e.url), a  }))) {  console.log("captured request", f, JSON.stringify(e));  const a = {    headers: s,    method: r,    url: f,    requestId: n,    templateRequestUrl: p,    type: e.type  };  i && (a.queryString = i), c(n, a), await lc()}

The code that does this

A user-started flow launches proof generation

Readable version
if ("start" === n) {  if (s.startHandled) return void r({    ok: !0  });  s.startHandled = !0, s.phase = xe.ATTESTING, Oc(), chrome.alarms.create(bc, {    periodInMinutes: .4  });  const {    userInfo: e  } = await w(["userInfo"]);  e || Kc().catch((e => {    console.log("ensureExtensionUserIdentity non-blocking error", e)  }));  const a = Object.assign({      isUserClick: "true"    }, s.formatAlgorithmParams),    c = await D(),    t = {      ...a,      clientType: c?.clientType || ""    };  await _({    activeRequestAttestation: JSON.stringify(a)  }), console.log("pageDecode-algorithmParams", a), chrome.runtime.sendMessage({    type: "algorithm",    method: "getAttestation",    params: t  })}

Exchange hosts named by the verified replay path

    • www.binance.com

    Private account and asset endpoints used as replay targets during exchange proof validation.

    • www.okx.com

    Private profile endpoint named in verification evidence for the same authenticated replay pattern.

Updated 2 November 2026oeiomhmbaapihbilkfkhmlajkeegnjhe