Is Email Finder-Kendo Sourcing Ninja safe?

High risk

Kendo Sourcing Ninja is high risk. The extension posts to kendoemailapp.com/kendoquerystatus2 when a LinkedIn profile loads, carrying the profile slug and fields name, company, title, company ID, member ID, user, location, platform, related companies, version, checksum.…

kendo ltdv6.704Chrome Web Store
78Risk
Who publishes it

Kendo.com ltd - 1 other listing from the same operator, none carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
kendo ltd
Declared legal entity
Kendo.com ltd
Registered address
Haparsa, Herzliya 4662017, IL
Registered contact
Kendo.com

Same store account

1 other listing published from this account, 30k+ users between them, none of them carrying a finding.

Shared hosts - 1 hostname

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

kendoemailapp.com
Also called by 3 other listings, including Kendo-Linkedin Email Finder

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

Publishers can request a review.

Findings

LinkedIn profile fields posted on profile visits

The extension posts to kendoemailapp.com/kendoquerystatus2 when a LinkedIn profile loads, carrying the profile slug and fields name, company, title, company ID, member ID, user, location, platform, related companies, version, checksum.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You open a LinkedIn member profile page.

The observed test page was the LinkedIn profile for williamhgates.

The extension did this

The extension posts the viewed profile identifier and profile fields to Kendo.

Dynamic analysis observed three POST requests after the profile page loaded.

Fields prepared for the profile-status request
  • Viewed profile
    urls=williamhgates

    Identifies the LinkedIn member profile you opened.

  • Profile identity fields
    nm="", comp="", title="", loc="" in the recorded request

    Can include the person's displayed name, title, company, company ID, member ID, and location when those values are available on the page.

  • LinkedIn context
    platform="", comps=""

    Adds context about the profile source and related companies so the profile visit can be categorized.

  • Extension metadata
    v=6.702 plus crc

    Ties the request to the installed extension version and a checksum generated by the extension.

Captured request
POSThttps://kendoemailapp.com/kendoquerystatus2

Dynamic analysis observed three POST requests after the LinkedIn profile page loaded.

Headers
Accept
application/json, application/xml, text/plain, text/html, *.*
Content-Type
application/json
The code that does this

Profile visit path from content script to Kendo

Readable version

Readable content script sends profile fields

deobfuscated/contentScript.bundle.js
chrome.runtime.sendMessage({  contentScriptQuery: _e.getstatus,  data: {    urls: i,    nm: c,    comp: m,    title: u,    compid: p,    id: f,    usr: h,    loc: g,    platform: b,    comps: y  }}, function(r) {  chrome.runtime.sendMessage({ contentScriptQuery: _e.gettop }, function(a) {    if (r) {      var v = "";      r && r.found && r.status && (v = r.status);      var x = document.createElement("div");      x.id = "kendo_elem";    }  });});

Readable background worker posts to kendoquerystatus2

deobfuscated/background.bundle.js
if (t.contentScriptQuery == o.getstatus) {  t.data && (t.data.v = chrome.runtime.getManifest().version);  j = i(JSON.stringify(t.data));  return t.data.crc = j, fetch("https://kendoemailapp.com/kendoquerystatus2", {    method: "POST",    headers: {      Accept: "application/json, application/xml, text/plain, text/html, *.*",      "Content-Type": "application/json"    },    body: JSON.stringify(t.data)  }).then(function(t) {    return t.json();  }).then(function(t) {    !c && t && t.widgettop && t.widgettop > 20 && (c = t.widgettop), n(t);  }).catch(function(t) {    return console.log("Error:", t);  }), !0;}
External destination for this claim
    • kendoemailapp.com

    Receives LinkedIn profile-status requests at /kendoquerystatus2.

Saved LinkedIn profile records posted to Kendo

Pressing save builds a LinkedIn profile record sent to kendoemailapp.com/kendoquery.

Fields can include URL, notes, status, subject, message, member name, pools, rate, mobile, website, email, title, skills, location, salary, memberID.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You save a LinkedIn profile with the extension.

The save button is disabled until the member-name field has content.

The extension did this

The extension sends a saved-profile record to Kendo.

The record combines values entered in the extension UI with fields read from the LinkedIn page.

Fields in the saved-profile object
  • Profile URL and IDs
    url=https://www.linkedin.com/in/jane-recruiter, memberID=123456789 (illustrative)

    Identifies the exact LinkedIn profile saved and links the record to LinkedIn member and talent IDs.

  • Entered recruiting notes
    notes=Follow up next week, status=Contacted, salary=120000 (illustrative)

    Carries the notes, status, subject, message body, pool, assignment, rate, and salary values entered while saving the profile.

  • Contact fields
    workemail=person@example.com, mobile=+1-415-555-0142 (illustrative)

    Can include work email, personal email, phone number, website, and the saved person's displayed name.

  • Profile page details
    title=Engineering Manager, location=San Francisco, imageURL=https://media.licdn.com/profile.jpg (illustrative)

    Adds LinkedIn page details such as title, company, location, skills, current job, and profile image URL.

Captured request
POSThttps://kendoemailapp.com/kendoquery

The source-confirmed request path posts the saved-profile object; unauthenticated dynamic testing did not capture a live body.

Headers
Accept
application/json, application/xml, text/plain, text/html, *.*
Content-Type
application/json
The code that does this

Save button path from content script to Kendo

Readable version

Readable content script builds the saved-profile object

deobfuscated/contentScript.bundle.js
onClick: function() {  B(!0);  var e = ke(Ye) ? Vi : Ye,    n = Oe.filter(function(e) { return e.active; }).map(function(e) { return "".concat(e.text); }),    r = n.join("~"),    a = Me(window.location.href),    o = {      notes: we,      url: ue,      status: e,      skiilsyears: oe,      subject: nt,      body: ot,      membername: K,      pools: r,      rate: null != Ke ? Ke : 0,      mobile: P,      website: I,      ___pools: n,      workemail: C,      email: M,      title: Te,      assigned: $,      skills: ne,      location: le,      job: S,      salary: A,      talentid: a    },    s = document.querySelector("[data-member-id]");  if (s && (o.memberID = s.attributes["data-member-id"].value), ((s = document.querySelector('[aria-label="Current company"]')) || (s = document.querySelector("[data-test-position-entity-company-link]"))) && (o.memberCompany = s.innerText.trim()), 1 == (s = document.getElementsByClassName("pv-top-card-profile-picture__image")).length) {    var l = s[0].attributes.getNamedItem("src");    l && (o.imageURL = l.value);  }  chrome.runtime.sendMessage({    contentScriptQuery: _e.postdata,    data: o  }, function(r) {    v(e), Ie(n), i(!1), B(!1);    var a = new CustomEvent("profilechanged", {      detail: { status: Ye, id: Le, pools: n, rate: Ke, assigned: $, nm: K, sk: ne, sky: oe }    }),    o = document.getElementById("kendoplugin");    o && o.dispatchEvent(a), t.onClose && t.onClose(Ye);  });}

Readable background worker posts to kendoquery

deobfuscated/background.bundle.js
if (t.contentScriptQuery == o.postdata) return t.data && (t.data.v = chrome.runtime.getManifest().version), fetch("https://kendoemailapp.com/kendoquery", {  method: "POST",  headers: {    Accept: "application/json, application/xml, text/plain, text/html, *.*",    "Content-Type": "application/json"  },  body: JSON.stringify(t.data)}).then(function(t) {  return t.json();}).then(function(t) {  n(t);}).catch(function(t) {  return console.log("Error:", t);}), !0;
External destination for this claim
    • kendoemailapp.com

    Receives saved LinkedIn profile records at /kendoquery.

LinkedIn profile details sent in a Kendo query URL

A LinkedIn profile update-button click sends its URL, candidate ID, company ID, and name to the background script, which GETs kendoemailapp.com/kendoquery2 with those as parameters.

Unauthenticated DA couldn't capture this; needs a login.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You press the extension update button on a LinkedIn profile.

The extension did this

The extension sends profile identifiers and the displayed name to Kendo in URL query parameters.

Fields placed in the Kendo query URL
  • LinkedIn profile URL
    https://www.linkedin.com/in/williamhgates

    This identifies the LinkedIn profile page you were viewing when you clicked the extension button.

  • Displayed profile name
    William H. Gates

    This can identify the LinkedIn member associated with the lookup.

  • Candidate ID
    18492517

    This links the request to the extension or Kendo record for that profile.

  • Company ID
    1035

    This adds company context to the profile lookup.

Captured request
GEThttps://kendoemailapp.com/kendoquery2?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fwilliamhgates&id=18492517&cmp=1035&mid=&nm=William%20H.%20Gates
The code that does this

Click handler and Kendo query construction

Readable version

Readable click-handler excerpt

contentScript.bundle.js
handleShow: function() {  var e;  (t.memberurl || t.candidateID) && (chrome.runtime.sendMessage({    contentScriptQuery: _e.allcandidates  }, function(e) {    ut(e)  }), t.candidateID && Ce(t.candidateID), U(!0), chrome.runtime.sendMessage({    contentScriptQuery: _e.getdata,    url: t.memberurl,    id: t.candidateID,    cmp: null !== (e = t.memberCompanyID) && void 0 !== e ? e : -1,    nm: t.memberName  }, function(e) {    if (U(!1), null != e && "" != e) {      var x = JSON.parse(e);      if (x.needlogin) return b(!0);    }  }))}

Readable background request branch

background.bundle.js
if (t.contentScriptQuery == o.getdata) return fetch("https://kendoemailapp.com/kendoquery2?url=".concat(encodeURIComponent(null !== (a = t.url) && void 0 !== a ? a : ""), "&id=").concat(encodeURIComponent(null !== (s = t.id) && void 0 !== s ? s : ""), "&cmp=").concat(encodeURIComponent(null !== (l = t.cmp) && void 0 !== l ? l : ""), "&mid=").concat(encodeURIComponent(null !== (m = t.memberid) && void 0 !== m ? m : ""), "&nm=").concat(encodeURIComponent(null !== (u = t.nm) && void 0 !== u ? u : ""))).then(function(t) {  return t.text()}).then(function(t) {  return n(t)}).catch(), !0;
Destination for the profile query
    • kendoemailapp.com

    Receives the profile lookup GET request at /kendoquery2 with LinkedIn profile fields in the query string.

+3 more findings not shown

Updated 30 September 2026kecadfolelkekbfmmfoifpfalfedeljo