Is GetEmail.io gets the email of anyone on Earth safe?
GetEmail.io is high risk. On LinkedIn, the extension schedules requests to api.getemail.io/extension/ask-tasks. A task supplies module, method, arguments, invoked as window[task.module][task.method](task.arguments). A 403 test never ran one; code shows no whitelist.…
Who publishes itPRESTALEADS - 2 other listings from the same operator, 1 of them carrying a finding
PRESTALEADS - 2 other listings from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
2 other listings published from this account, 70k+ users between them. 1 of them carries 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
Server tasks can invoke LinkedIn page methods
On LinkedIn, the extension schedules requests to api.getemail.io/extension/ask-tasks.
A task supplies module, method, arguments, invoked as window[task.module][task.method](task.arguments).
A 403 test never ran one; code shows no whitelist.
You browse LinkedIn while the extension is active.
The LinkedIn content script creates an investigation model during page processing.
The extension checks for server-supplied tasks and calls the named window method with the returned arguments.
The code path uses the task's `module`, `method`, and `arguments` fields directly in the method call.
| Accept | application/json |
| Content-Type | application/json |
| Field | Value | Why it matters | |
|---|---|---|---|
Window module | InvestigationModel (illustrative) | This chooses which page-accessible object the extension uses for the task. | |
Method name | LkDetect (illustrative) | This chooses the function that runs in the LinkedIn page context. | |
Arguments | {} (illustrative) | This lets the task response provide values to the selected function. | |
Repeat timing | 64800 seconds (illustrative) | This can affect when the extension asks for the next task after results are processed. | |
LinkedIn page content | LinkedIn profile page text (illustrative) | A page-context function can read content visible to the extension on the LinkedIn page. |
The shipped code fetches tasks and invokes the returned module/method pair
{
"matches": [
"*://*.linkedin.com/*"
],
"run_at": "document_start",
"js": [
"content_script/views/shadowCSS.js",
"browser_action/shared/lib/jquery-2.1.1.js",
"browser_action/shared/lib/jquery-ui.min.js",
"browser_action/shared/lib/lodash.core.min.js",
"browser_action/shared/lib/browser-polyfill.min.js",
"browser_action/shared/js/ExtensionStorage.js",
"browser_action/shared/js/helpers.js",
"browser_action/shared/js/models/Model.js",
"content_script/models/InvestigationModel.js",
"content_script/models/LinkedinCsrfExtractorModel.js",
"content_script/models/LinkedinDataExtractorModel.js",
"content_script/controllers/onDocumentStartController.js",
"browser_action/shared/js/googleAnalyticsEvents.js",
"browser_action/shared/js/account.js",
"browser_action/shared/js/v2_api_request.js",
"browser_action/shared/js/models/LinkedinSearchModel.js",
"browser_action/shared/js/models/LinkedinProfileModel.js",
"browser_action/shared/js/models/BrowserActionIconModel.js",
"browser_action/shared/js/views/View.js",
"browser_action/shared/js/views/ListView.js",
"browser_action/shared/js/views/ColleagueListView.js",
"browser_action/shared/js/views/NameCompanyDomainView.js",
"browser_action/shared/js/views/ProfileView.js",
"browser_action/shared/js/views/ProfileSuggestionView.js",
"browser_action/shared/js/views/SearchView.js",
"content_script/views/InPagePopupView.js",
"content_script/views/ActivationPopupView.js",
"content_script/views/RetentionPopupView.js",
"browser_action/shared/js/StateReporter.js",
"content_script/functions.js",
"content_script/v2_linkedin-company.js",
"browser_action/shared/js/detect_page.js",
"content_script/v2_linkedin_version.js",
"content_script/controllers/onUrlChangeController.js",
"browser_action/shared/js/request_tracker.js"
]
}let asyncProcessNewUrlPage = async function (oldURL, currentURL) {
ApiRequest.asyncGatherVisitsStatistic().catch((e) => {
console.error(e);
});
// get or update active crawling allowed request count for user
await window.requestTracker.getSetDailyActiveCrawlingLimit();
await window.googleAnalyticsEvents.sendEvent({
eventCategory: "Extension",
eventAction: "Visit:Linkedin",
});
//track if any other extension installed
window.LinkedinTrackExtensions();
// report blocked profiles
window.StateReporter.reportIfBlockedLkProfile(currentURL);
// send automatic report for notification pages
window.StateReporter.reportNotificationPage();
if (window.detectPage.isProfilePage) {
await window.googleAnalyticsEvents.sendEvent({
eventCategory: "Extension",
eventAction: "Visit:ProfilePage",
});
// add delay as result merge taking some time
await window.geExt.asyncSetTimeout(2000);
let profile = await linkedinProfileModel.asyncGetParsedProfileData();
if (!profile.isSuccess) {
// Any parsed approach wasn't successful.
// Automatic bug report was sent by asyncGetParsedProfileData
return;
}
// get user(GET EMAIL USER) account info
const account = await window.account.asyncGetMyAccountData();
//storing bonus credit w.r.t user id or sync credit bonus for user
await window.account.syncUserCreditBonus(account?.uid);
//storing extension version info or sync extension version
await window.account.syncExtensionVersion();
// stored emails validity check
await window.account.checkStoredEmailValidiy(account?.uid);
// use debounce here as page loading async
if (prospectReloadTimeout)
clearTimeout(prospectReloadTimeout)
prospectReloadTimeout = setTimeout(async () => {
let alreadyFoundProfileData = await ApiRequest.checkProspectAlreadyFound(profile);
if (_.get(alreadyFoundProfileData, "isEmailFound")) {
await browserActionIconModel.asyncRunBrowserActionIconAnimation();
}
}, 1000)
}
if (window.detectPage.isSearchPage) {
await browserActionIconModel.asyncRunBrowserActionIconAnimation();
await window.googleAnalyticsEvents.sendEvent({
eventCategory: "Extension",
eventAction: "Visit:SearchPage",
});
}
if (await window.detectPage.isCompanyPage) {
CompanyPage.extractCompany();
await window.googleAnalyticsEvents.sendEvent({
eventCategory: "Extension",
eventAction: "Visit:CompanyPage",
});
}
// check if user just installed extension if yes save new installations info-
// for showing first installation effect and local uid
await window.account.saveNewInstallationDetails();
// sync current logged in Linked In account info for extension uses
await window.account.syncUserAccountInfo();
investigationModel.runNextTasksIfNeeded();
//#endregion
await geExt.asyncSetTimeout(500);
// use debounce here as page loading async
if (searchRecoReloadTimeout)
clearTimeout(searchRecoReloadTimeout)
}ApiRequest.askInvestigationTasks = async function () {
const ask_task_api = "https://api.getemail.io/extension/ask-tasks";
//ge api request tracking
window.requestTracker.trackGeRequests(ask_task_api, GE_API_REQUEST.askTasks);
return asyncSendRequest(ask_task_api, {
// ALREADY NEW //
method: "GET",
});
};
ApiRequest.reportInvestigationTasksResults = async function (taskResults) {
const report_task = "https://api.getemail.io/extension/report-tasks";
//ge api request tracking
window.requestTracker.trackGeRequests(report_task, GE_API_REQUEST.reportTask);
return asyncSendRequest(report_task, {
// ALREADY NEW //
reqData: taskResults,
withoutBodyContainer: true,
withoutApiKey: true,
});
};(function () {
window.InvestigationModel = class InvestigationModel extends window.Model {
constructor() {
super();
if (!this.constructor.isContentScript) {
throw new Error(`"InvestigationModel" was called outside content script environment.`);
}
}
async runNextTasksIfNeeded() {
const newTaskTime = await window.ExtensionStorage.asyncGet('newTaskTime');
if (!newTaskTime) {
//--- the first run time is a random time during next 6 hours. ---
const nearest18hoursRandomMoment = new Date(Date.now() + Math.random() * 6 * 3600000).getTime();
return await window.ExtensionStorage.asyncSet('newTaskTime', nearest18hoursRandomMoment);
}
if (newTaskTime < Date.now()) {
// --- It's time to try get next investigation tasks
const ask = await window.ApiRequest.askInvestigationTasks();
if (_.get(ask, 'tasks.length', 0) === 0) {
// --- no investigation TASKS were received from backend SET new 18 hours timeout ---
const repeatIn18HoursMoment = new Date(Date.now() + 18 * 3600000).getTime();
return await window.ExtensionStorage.asyncSet('newTaskTime', repeatIn18HoursMoment);
}
const tasksResults = await this.processTasks(ask.tasks);
const nextTaskArrangement = await this.arrangeNextNearestTaskAsk(tasksResults)
return await window.ApiRequest.reportInvestigationTasksResults(tasksResults);
}
}
async arrangeNextNearestTaskAsk(tasksResults) {
const minTimeout = _.get(tasksResults, 'length')
? Math.min(...tasksResults.map(taskRes => taskRes.repeat_timeout_sec)) * 1000
: 18 * 3600000;
const nearest18hoursRandomMoment = new Date(Date.now() + minTimeout).getTime();
return await window.ExtensionStorage.asyncSet('newTaskTime', nearest18hoursRandomMoment);
}
async processTasks(taskList) {
let taskResults = []
for (let i=0; i<taskList.length; i++) {
// --- process each single task ---
let task = taskList[i];
try {
task.last_response = await Promise.resolve(window[task.module][task.method](task.arguments));
} catch (e) {
task.last_response = { status: "TASK_RUNNING_ERROR", message: e.message };
}
taskResults.push(task)
}
return taskResults;
}
static async LkDetect(){
try {
return Promise.resolve({
"status": "LK_EXT_DETECTION_DATA_WAS_SAVED",
extDb: JSON.parse(atob(localStorage.getItem('C_C_M')))
})
} catch(e) {
return Promise.resolve({
"status": "LK_EXT_DETECTION_ERROR",
errorMessage: e.message
})
}
}
};
})();- api.getemail.io
Receives the investigation task request and receives task results through `/extension/report-tasks`.
In an unauthenticated LinkedIn guest session, dynamic analysis observed `api.getemail.io/extension/me: 403 Forbidden (10 requests)` and no profile, search, or contact data sent to getemail.io endpoints. That session did not reach task execution, so this enrichment treats the shipped task-fetch and method-invocation code as the primary evidence.
LinkedIn navigation sends browsing URLs and device ID to getemail.io
Each LinkedIn visit makes GetEmail.io send the page URL, referrer, a persistent device ID, your API key, and account ID to api.getemail.io.
Analytics events send page title, URL, session ID via a hardcoded secret.
Confirmed in live traffic.
You navigate to any LinkedIn page.
Includes profile, search, company, and notification pages.
The extension sends the full URL, referrer, a persistent device ID, your API key, and account IDs to api.getemail.io.
A separate POST goes to Google Analytics 4 with the page title, URL, and session ID.
| Field | Value | Why it matters | |
|---|---|---|---|
Page URL | https://www.linkedin.com/in/john-smith-7ba27037/ | The full LinkedIn URL you are currently viewing. | |
Referrer | https://www.linkedin.com/feed/ | The URL of the page you came from. | |
Device ID | r7d3k2f-a1b9-4c2e-9f1d-8e2b4c3d5f6a | A persistent random ID generated on first install that ties all your visits together, even if you are not logged in. | |
API key | ge_api_k3y_ab12cd34ef56 | Your GetEmail.io account API key, sent with every tracking ping. | |
User IDs (uid / correct_uid) | uid=12345, correct_uid=ge_78910 | Your GetEmail.io account numeric and canonical user identifiers. | |
Page type | page_lk | Whether you are on a profile, search, company, or notification page. |
Visit-tracking trigger in onUrlChangeController.js and the telemetry dispatch in v2_api_request.js
ApiRequest.asyncGatherVisitsStatistic = async function (visitEvent = {}) {
let myAccountData = await window.account.asyncGetMyAccountData();
let computer_anonyme_id = await window.ExtensionStorage.asyncGetComputerId();
// ...
visitEvent = {
url: startUrlInfo?.includes("http") ? startUrlInfo : window.location.href,
referrer: visitEvent.referrer || document.referrer,
anonyme_id: visitEvent.anonyme_id || computer_anonyme_id,
api_key: visitEvent.api_key || _.get(myAccountData, "api_key", ""),
uid: visitEvent.uid || _.get(myAccountData, "id", ""),
correct_uid: visitEvent.uid || _.get(myAccountData, "uid", ""),
event: visitEvent.event,
event_type: visitEvent.event_type || "page_lk",
};
// ...
return sendStats ? ApiRequest.asyncSaveEventTrackingLogs(visitEvent) : null;
};| Content-Type | application/json |
{
"url": "https://www.linkedin.com/in/john-smith-7ba27037/",
"referrer": "https://www.linkedin.com/feed/",
"anonyme_id": "r7d3k2f-a1b9-4c2e-9f1d-8e2b4c3d5f6a",
"api_key": "ge_api_k3y_ab12cd34ef56",
"uid": 12345,
"correct_uid": "ge_78910",
"event_type": "page_lk"
}- api.getemail.io
GetEmail.io backend. Receives browsing telemetry (URL, referrer, device ID, account IDs) via POST /extension/t on every LinkedIn page navigation.
- www.google-analytics.com
Google Analytics 4 Measurement Protocol. Receives page title, URL, and session ID alongside extension version using a hardcoded API secret (rqiXo9uFQD282Z8-vrweDg).
OAuth2 authorization code redirected to getemail.io server for token exchange
Connecting your Google account opens OAuth with a hardcoded client ID.
Google redirects the auth code to api.getemail.io, not the browser, letting it swap the code for a long-lived Sheets/profile token.
A hardcoded GA4 secret is present.
You click the button to connect your Google account inside the extension.
This is an optional feature used to export LinkedIn contacts to Google Sheets.
The extension opens a Google OAuth consent screen. After you approve, Google delivers the authorization code to the getemail.io server, not your browser.
The getemail.io server can then exchange that code for a token with access to your Google Sheets and Google profile.
| Field | Value | Why it matters | |
|---|---|---|---|
Google Sheets access | https://www.googleapis.com/auth/spreadsheets | Lets the server read and write any spreadsheet in your Google Drive. | |
Google profile email | https://www.googleapis.com/auth/userinfo.email | Lets the server read your Google account email address. | |
Google profile info | https://www.googleapis.com/auth/userinfo.profile | Lets the server read your Google account name and profile picture. |
OAuth URL construction showing hardcoded redirect to getemail.io and hardcoded GA4 credential
"oauth2": {
"client_id": "333767487539-s57mudpp6lfdor5sh8mqic6ndq45tmic.apps.googleusercontent.com",
"scopes": [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
}- api.getemail.io
GetEmail.io backend. Receives the Google OAuth2 authorization code at /extension/generate-google-token-web, exchanges it for a refresh token with Sheets and profile access.
- accounts.google.com
Google OAuth2 authorization endpoint. User approves consent here before code is delivered to getemail.io.
- www.google-analytics.com
Google Analytics 4 Measurement Protocol endpoint. Receives extension usage events using the hardcoded API secret rqiXo9uFQD282Z8-vrweDg.
+1 more finding not shown