Is GetEmail.io gets the email of anyone on Earth safe?

High risk

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.…

getemail.iov2.0.433Chrome Web Store
75Risk
Who publishes it

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.

Publisher
getemail.io
Declared legal entity
PRESTALEADS
Registered address
28 BD DU COLOMBIER, RENNES, BRETAGNE 35000, FR
Registered contact
MR Nicolas Bahout

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.

signalhire.com
Also called by 4 other listings, including SignalHire - find email or phone number, SignalHire

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You browse LinkedIn while the extension is active.

The LinkedIn content script creates an investigation model during page processing.

The extension did this

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.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://api.getemail.io/extension/ask-tasks
The shipped request path expects a JSON response with a `tasks` array. No task response body was recorded in the unauthenticated dynamic-analysis session.
Headers
Acceptapplication/json
Content-Typeapplication/json
03EvidenceFIELD TABLE
Fields that control the investigation task call
FieldValueWhy 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.
04EvidenceCODE COMPARE
The code that does this

The shipped code fetches tasks and invokes the returned module/method pair

What it actually does
LinkedIn pages load the investigation task codemanifest.json
{
  "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"
  ]
}
The LinkedIn controller calls the task runnercontent_script/controllers/onUrlChangeController.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)
}
The API helper defines the task endpointsbrowser_action/shared/js/v2_api_request.js
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,
    });
  };
The task processor invokes the returned window methodcontent_script/models/InvestigationModel.js
(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
                })
            }
        }
    };


})();
05EvidenceTHIRD PARTY LIST
External host involved in the task system
  • api.getemail.io

    Receives the investigation task request and receives task results through `/extension/report-tasks`.

06EvidencePLAIN NOTE
Dynamic-analysis caveat

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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to any LinkedIn page.

Includes profile, search, company, and notification pages.

The extension did this

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.

02EvidenceFIELD TABLE
Fields sent to api.getemail.io/extension/t on each LinkedIn page visit
FieldValueWhy 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-8e2b4c3d5f6aA 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_ab12cd34ef56Your GetEmail.io account API key, sent with every tracking ping.
User IDs (uid / correct_uid)
uid=12345, correct_uid=ge_78910Your GetEmail.io account numeric and canonical user identifiers.
Page type
page_lkWhether you are on a profile, search, company, or notification page.
03EvidenceCODE COMPARE
The code that does this

Visit-tracking trigger in onUrlChangeController.js and the telemetry dispatch in v2_api_request.js

What it actually does
Data assembled in asyncGatherVisitsStatistic — v2_api_request.js lines 926–973browser_action/shared/js/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;
  };
04EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.getemail.io/extension/t
Confirmed live in dynamic analysis (observed requests; same endpoint in v2.0.420).
Headers
Content-Typeapplication/json
Body
{
  "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"
}
05EvidenceTHIRD PARTY LIST
Endpoints receiving telemetry on each LinkedIn page visit
  • 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).

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-522
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

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 did this

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.

02EvidenceFIELD TABLE
OAuth scopes granted to the getemail.io server
FieldValueWhy it matters
Google Sheets access
https://www.googleapis.com/auth/spreadsheetsLets the server read and write any spreadsheet in your Google Drive.
Google profile email
https://www.googleapis.com/auth/userinfo.emailLets the server read your Google account email address.
Google profile info
https://www.googleapis.com/auth/userinfo.profileLets the server read your Google account name and profile picture.
03EvidenceCODE COMPARE
The code that does this

OAuth URL construction showing hardcoded redirect to getemail.io and hardcoded GA4 credential

What it actually does
manifest.json — hardcoded OAuth2 client_id and scopesmanifest.json
"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"
    ]
  }
04EvidenceTHIRD PARTY LIST
Endpoints receiving the OAuth credentials
  • 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

Updated 30 September 2026hbnjdgffjfjbkdoghlpkedjfoddlgbge