Is Email Finder-Kendo Sourcing Ninja safe?
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.…
Who publishes itKendo.com ltd - 1 other listing from the same operator, none carrying a finding
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.
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.
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
You open a LinkedIn member profile page.
The observed test page was the LinkedIn profile for williamhgates.
The extension posts the viewed profile identifier and profile fields to Kendo.
Dynamic analysis observed three POST requests after the profile page loaded.
- Viewed profileurls=williamhgates
Identifies the LinkedIn member profile you opened.
- Profile identity fieldsnm="", 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 contextplatform="", comps=""
Adds context about the profile source and related companies so the profile visit can be categorized.
- Extension metadatav=6.702 plus crc
Ties the request to the installed extension version and a checksum generated by the extension.
Dynamic analysis observed three POST requests after the LinkedIn profile page loaded.
- Accept
- application/json, application/xml, text/plain, text/html, *.*
- Content-Type
- application/json
Profile visit path from content script to Kendo
Readable content script sends profile fields
deobfuscated/contentScript.bundle.jschrome.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.jsif (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;}- 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
You save a LinkedIn profile with the extension.
The save button is disabled until the member-name field has content.
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.
- Profile URL and IDsurl=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 notesnotes=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 fieldsworkemail=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 detailstitle=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.
The source-confirmed request path posts the saved-profile object; unauthenticated dynamic testing did not capture a live body.
- Accept
- application/json, application/xml, text/plain, text/html, *.*
- Content-Type
- application/json
Save button path from content script to Kendo
Readable content script builds the saved-profile object
deobfuscated/contentScript.bundle.jsonClick: 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.jsif (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;- 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
You press the extension update button on a LinkedIn profile.
The extension sends profile identifiers and the displayed name to Kendo in URL query parameters.
- LinkedIn profile URLhttps://www.linkedin.com/in/williamhgates
This identifies the LinkedIn profile page you were viewing when you clicked the extension button.
- Displayed profile nameWilliam H. Gates
This can identify the LinkedIn member associated with the lookup.
- Candidate ID18492517
This links the request to the extension or Kendo record for that profile.
- Company ID1035
This adds company context to the profile lookup.
Click handler and Kendo query construction
Readable click-handler excerpt
contentScript.bundle.jshandleShow: 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.jsif (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;- kendoemailapp.com
Receives the profile lookup GET request at /kendoquery2 with LinkedIn profile fields in the query string.
+3 more findings not shown