Is Primus safe?

High risk

Primus is high 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.7Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Attestation flow copies headers and cookies

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.

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

02EvidenceFIELD TABLE
Fields the attestation path can copy from matched requests
FieldValueWhy it matters
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.
03EvidenceCODE COMPARE
The code that does this

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

What it actually does
Request listeners registered for all URLsbackground.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 storedbackground.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 presentbackground.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 parametersbackground.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 enginebackground.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 callAlgorithmoffscreen.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
}
04EvidenceTHIRD PARTY LIST
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.

05EvidenceNETWORK CAPTURE
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.
SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Primus replays signed-in exchange API requests

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

The confirmed path covers private Binance-style endpoints; no body was recorded.

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

02EvidenceFIELD TABLE
Fields used in the replayed exchange request
FieldValueWhy it matters
Exchange session cookies
Cookie: Binance session cookies redactedKeeps the replayed request tied to your signed-in exchange session.
Captured request headers
User-Agent: Mozilla/5.0; Accept: application/jsonPreserves 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-assetPoints the replay at an account API that returns data about your exchange profile or assets.
Proof-flow context
activeRequestAttestation with isUserClick trueLinks the replayed exchange response to the proof request that is being generated.
03EvidenceNETWORK CAPTURE
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
Cookieredacted active exchange session cookies
04EvidenceCODE COMPARE
The code that does this

Replay helper keeps headers and browser credentials

What it actually does
Readable fetch option builderbackground.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 headersbackground.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)
05EvidenceCODE COMPARE
The code that does this

The extension stores matching request headers before replay

What it actually does
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()
}
06EvidenceCODE COMPARE
The code that does this

A user-started flow launches proof generation

What it actually does
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
  })
}
07EvidenceTHIRD PARTY LIST
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 17 September 2026oeiomhmbaapihbilkfkhmlajkeegnjhe