Is SignalHire - find email or phone number safe?

Medium risk

SignalHire is medium risk. After SignalHire app data is populated, the background service reads a server-provided WebSocket host and token, opens a Socket.IO WebSocket with it, routing pushed messages into Redux handlers. Only the status request was captured.

signalhire.comv5.12.0Chrome Web Store
45Risk
Who publishes it

SignalHire Sp. z o.o. - no other listings under this identity, 1 shared hostname

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

Publisher
signalhire.com
Declared legal entity
SignalHire Sp. z o.o.
Registered address
ZŁOTA Street, No. 75A, Loc. 7, WARSZAWA 00-819, PL
Registered contact
SignalHire Sp. z o.o.

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 5 other listings, including GetEmail.io, GetEmail.io Legacy, SignalHire - find email or phone number

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

Publishers can request a review.

Findings

SignalHire code opens a token-authenticated WebSocket

After SignalHire app data is populated, the background service reads a server-provided WebSocket host and token, opens a Socket.IO WebSocket with it, routing pushed messages into Redux handlers.

Only the status request was captured.

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

You use the extension with SignalHire account data present in browser storage.

That app data is populated by the extension after it calls the SignalHire status endpoint.

The extension did this

The background service opens a token-authenticated WebSocket to the host named in that stored data.

Messages from that channel are parsed and routed into extension handlers.

Captured request
GEThttps://www.signalhire.com/ext-api/status

200 OK observed during dynamic analysis; the test profile was not signed in, so no WebSocket host or token was returned.

Fields that enable and drive the WebSocket channel
  • SignalHire user ID
    sync:app-data.userId

    This ties the WebSocket session to your SignalHire account inside the extension.

  • WebSocket host
    sync:app-data.websocket.host

    This decides which server receives the persistent Socket.IO connection from your browser.

  • WebSocket token
    sync:app-data.websocket.token

    This authenticates the persistent channel so pushed messages can be associated with your extension session.

  • Pushed route name
    app.commands_topic

    This chooses which extension handler processes a message sent over the socket.

  • Message acknowledgement ID
    message_id

    This lets the server match a pushed message with the acknowledgement sent back by the extension.

The code that does this

Socket setup and server-pushed message routing in background.js

Readable version

Readable Socket.IO setup and process_clint handler

background.js
  var vp = 1e4,    yp = {      CONNECT: `connect`,      DISCONNECT: `disconnect`,      PROCESS_CLINT: `process_clint`    },    bp = class {      static socket = null;      static isConnectedState = !1;      static messageHandler = null;      static get isConnected() {        return this.isConnectedState      }      static onMessage(e) {        this.messageHandler = e      }      static handleConnect = () => {        this.isConnectedState = !0      };      static handleDisconnect = () => {        this.isConnectedState = !1      };      static handleProcessClint = e => {        let {          route: t,          data: n,          message_id: r,          accept_flag: i        } = e;        try {          let e = JSON.parse(n);          if (i && this.socket.emit(yp.PROCESS_CLINT, {              message_id: r,              accept: !0            }), S(e)) return;          this.messageHandler && this.messageHandler({            route: t,            data: e          })        } catch (t) {          let n = t;          k({            level: `error`,            message: `WebsocketsService.handleProcessClint: ${n.message}`,            context: {              error: n,              params: e            }          })        }      };      static init = e => {        let t = !S(e) && !S(e.userId) && !S(e.websocket) && !S(e.websocket?.host) && !S(e.websocket?.token);        if (!t && !S(this.socket)) {          this.socket.disconnect(), this.socket.off(yp.CONNECT, this.handleConnect), this.socket.off(yp.DISCONNECT, this.handleDisconnect), this.socket.off(yp.PROCESS_CLINT, this.handleProcessClint), this.socket = null, this.isConnectedState = !1;          return        }!t || !S(this.socket) || (this.socket = _p(e.websocket.host, {          reconnectionDelayMax: vp,          auth: {            user_id: e.userId,            token: e.websocket.token          },          transports: [`websocket`]        }), this.socket.on(yp.CONNECT, this.handleConnect), this.socket.on(yp.DISCONNECT, this.handleDisconnect), this.socket.on(yp.PROCESS_CLINT, this.handleProcessClint))      }

Readable route dispatcher and storage watcher

background.js
    }, f_ = ({      data: e    }) => {      switch (e.type) {        case `rereadAppData`:          xg.dispatch(Cp()), xg.dispatch(Qh());          break;        default:          e.type;          break      }    }, p_ = async ({      route: e,      data: t    }) => {      switch (e) {        case `app_profiles_job_changes_topic`:          xg.dispatch(xc({            profiles: t?.data.map(e => ({              id: e.id,              changes: {                jobProjects: e.jobProjects              }            }))          }));          break;        case `app_get_contacts_updates_topic`:          xg.dispatch(bc({            id: t?.data?.uid,            changes: {              contactsFetching: t?.data?.pending ? `pending` : `done`,              contacts: t?.data?.contacts,              social: t?.data?.social            }          }));          break;        case `app_get_contacts_credits_topic`:          xg.dispatch(ig({            type: `getContacts`,            data: t?.data          }));          break;        case `app_notification_topic`:          d_(t);          break;        case `app.commands_topic`:          f_(t);          break;        case `app_bulk_profiles_parse`: {          let e = cm(xg.getState(), t?.data?.searchId),            n = e?.parseStatus === `succeeded` && e?.pingStatus === `succeeded` && e?.fetchStatus === `succeeded`;          if (S(e) || n) return;          let r = pc(`candidateId`)(t.data?.candidates ?? []),            i = [...e.profileIds, ...r],            a = {              tabId: e.id,              tabUrl: e.tabUrl,              profileIds: i            };          if (i.length && xg.dispatch(Ru({              badge: {                id: e.id,                text: `${i.length}`              }            })), t?.data?.pending) {            xg.dispatch(Fc(a));            return          }          xg.dispatch(Ic(a)), xg.dispatch(Jo(a));          break        }        default:          break      }    }, m_ = () => {      ve.get(`sync:app-data`).then(bp.init), ve.watch(`sync:app-data`, bp.init), bp.onMessage(p_)

Readable status response storage

background.js
  function* Lp() {    yield ia(Pp), yield V(Cp())  }  function* Rp() {    try {      yield V(kp());      let e = yield wo(2, Np, Le);      yield U(ve.set, `sync:app-data`, e.data), yield V(Ap(e.data)), yield U(wp.setUninstallURL, Dp(e?.data?.instanceId)), Ip.cancelById(Fp);      let t = yield ta(Lp);      Ip.save(Fp, t)    } catch {      yield V(jp())    }  }
What's stored on your device

When this stored app data exists, it gives the background service the account ID, destination, and token to start the socket session.

Location
chrome.storage.sync key 'app-data'
Contents
{  "userId": "read before socket auth.user_id is set",  "websocket": {    "host": "read as the Socket.IO destination",    "token": "read before socket auth.token is set"  },  "instanceId": "also stored from the status response and used elsewhere by the extension"}
Network destinations involved in this path
    • www.signalhire.com

    SignalHire status endpoint supplying app data stored by the extension when the account is available; the data contains the WebSocket host used later by the background service.

Updated 30 September 2026aeidadjdhppdffggfgjpanbafaedankd