Is SignalHire - find email or phone number safe?
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.
Who publishes itSignalHire Sp. z o.o. - no other listings under this identity, 1 shared hostname
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.
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
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
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 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.
200 OK observed during dynamic analysis; the test profile was not signed in, so no WebSocket host or token was returned.
- SignalHire user IDsync:app-data.userId
This ties the WebSocket session to your SignalHire account inside the extension.
- WebSocket hostsync:app-data.websocket.host
This decides which server receives the persistent Socket.IO connection from your browser.
- WebSocket tokensync:app-data.websocket.token
This authenticates the persistent channel so pushed messages can be associated with your extension session.
- Pushed route nameapp.commands_topic
This chooses which extension handler processes a message sent over the socket.
- Message acknowledgement IDmessage_id
This lets the server match a pushed message with the acknowledgement sent back by the extension.
Socket setup and server-pushed message routing in background.js
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()) } }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'
{ "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"}- 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.