Is RocketReach Chrome Extension safe?
RocketReach is medium risk. Dynamic analysis captured a POST from RocketReach's service worker to plugin.rocketreach.co/browser-extension/v1/m5s at startup, before use. The base64 body decoded to metric events, flushed every 10s, tagged with endpoints like 'user'.…
Who publishes itRocketReach LLC - no other listings under this identity, 1 shared hostname
RocketReach LLC - 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
Telemetry Sent on Every Extension Action
Dynamic analysis captured a POST from RocketReach's service worker to plugin.rocketreach.co/browser-extension/v1/m5s at startup, before use.
The base64 body decoded to metric events, flushed every 10s, tagged with endpoints like 'user'.
You install the extension and open Chrome.
The service worker begins queuing usage metric events and posts them base64-encoded to RocketReach's servers every 10 seconds.
No user interaction is required. Events are sent even before you log in or search for anyone.
| Content-Type | text/plain;charset=UTF-8 |
eyJtZXRyaWNzIjpbeyJtZXRyaWNfdHlwZSI6ImluY3JlbWVudCIsIm5hbWUiOiJmZXRjaCIsInRhZ3MiOlt7InN0YXR1cyI6InN1Y2Nlc3MiLCJlbmRwb2ludCI6InVzZXIifV19LHsibWV0cmljX3R5cGUiOiJpbmNyZW1lbnQiLCJuYW1lIjoidXJsX3Jlc29sdXRpb24iLCJ0YWdzIjpbeyJzdGF0dXMiOiJub19tYXRjaCIsInBhZ2VfdHlwZSI6ImdlbmVyaWMifV19XX0=
The POST body is base64-encoded, so it does not appear as readable JSON in browser DevTools or network monitors. Decoding the wire value reveals the full metrics array.
{
"metrics": [
{
"metric_type": "increment",
"name": "fetch",
"tags": [
{
"status": "success",
"endpoint": "user"
}
]
},
{
"metric_type": "increment",
"name": "url_resolution",
"tags": [
{
"status": "no_match",
"page_type": "generic"
}
]
}
]
}| Field | Value | Why it matters | |
|---|---|---|---|
Event type | increment | Whether this is a count event (increment) or a measured value (histogram). | |
Event name | url_resolution | Which action or API call triggered the event, e.g. whether the extension tried to resolve the current page's URL. | |
Tags: status | no_match | The outcome of the action, such as whether an API call succeeded or the URL matched a known profile page. | |
Tags: endpoint | person_search | Which RocketReach API endpoint was involved, reveals which features you used (person search, company search, user profile, etc.). | |
Tags: page type | What type of page you were on when the event fired (LinkedIn, recruiter portal, generic site). |
The metrics service from the extension's minified source:
enum MetricType {
increment = "increment",
histogram = "histogram",
}class MetricsService {
static async build(): Promise<MetricsService> {
const svc = new MetricsService();
svc._apiService = await ApiService.build();
svc._endpointUrl = new URL("/browser-extension/v1/m5s", svc._apiService.BASE_URL);
svc._featureFlags = await FeatureFlagService.build();
return svc;
}
encodedBody(): string {
// base64-encodes the entire metrics batch — not encrypted, trivially reversible
return btoa(JSON.stringify({ metrics: this._batch }));
}
_getBatchDelaysMS(): number {
return 10000; // flush every 10 seconds
}
}- plugin.rocketreach.co
Primary RocketReach API host. Receives the metrics batch at /browser-extension/v1/m5s. Sole permitted externally_connectable origin in the manifest.
Decodes a base64 metrics batch body from the RocketReach extension and prints it as formatted JSON. Paste the raw POST body from DevTools and run with Node.js.
#!/usr/bin/env node
// rocketreach-metrics-decoder.js
// Decodes a base64-encoded metrics batch captured from RocketReach's
// POST to plugin.rocketreach.co/browser-extension/v1/m5s
//
// Usage: node rocketreach-metrics-decoder.js <base64-string>
// or: echo '<base64>' | node rocketreach-metrics-decoder.js
const b64 = process.argv[2] || require('fs').readFileSync('/dev/stdin', 'utf8').trim();
try {
const json = Buffer.from(b64, 'base64').toString('utf8');
const parsed = JSON.parse(json);
console.log(JSON.stringify(parsed, null, 2));
console.log(`\nTotal events in batch: ${parsed.metrics?.length ?? 0}`);
} catch (err) {
console.error('Failed to decode:', err.message);
console.error('Expected: a base64 string from the POST body of /browser-extension/v1/m5s');
process.exit(1);
}- 1Open DevTools Network tab with RocketReach installed.
- 2Filter by 'm5s'.
- 3Copy the POST body.
- 4Run: node rocketreach-metrics-decoder.js '<base64>'.
- 5Decoded metrics print to stdout.
Remote Feature Flags Control Extension Behavior
Dynamic analysis captured GETs to plugin.rocketreach.co/browser-extension/v1/rr-everywhere at startup, before login.
It also fetches config from /v1/user/ff3p hourly, gating all-page activation, flippable externally by that host.
You open Chrome with RocketReach installed, even without logging in.
The extension contacts plugin.rocketreach.co to fetch feature flags that determine which capabilities are active on your browser.
This happens unconditionally on startup. The extension also repeats the fetch every 60 minutes via a Chrome alarm.
| Field | Value | Why it matters | |
|---|---|---|---|
rr-everywhere toggle | {"enabled": false} | Whether the extension activates on every website you visit, not just supported recruitment platforms. | |
Feature flags store | {"lastRefresh": 1744640400000, "features": {"new_profile_ui": true, "bulk_export": false}} | A JSON object stored in chrome.storage.local that gates which extension features are enabled for your account. | |
ff3p config | {"features": {"sidebar_v2": "enabled", "autocomplete": false}} | Third-party feature configuration fetched from /v1/user/ff3p, controls additional feature variants tied to your account. |
Feature flag fetch and alarm registration from the minified source:
// Registers a Chrome alarm named 'util.FeatureFlag' that fires every 60 minutes.
async registerAlarm() {
if (!chrome.alarms) return;
if (await chrome.alarms.get(FeatureFlagService.AlarmName)) return; // already registered
chrome.alarms.create(FeatureFlagService.AlarmName, { periodInMinutes: this._intervalMinutes }); // 60 min
}// Fetches whether the extension should activate on all sites.
// Called unconditionally — no login check, fires on every extension load.
async getAndSaveEverywhereState() {
const url = new URL("/browser-extension/v1/rr-everywhere", this._apiService.BASE_URL);
const response = await fetch(url, { method: "GET" });
const data = await response.json();
const isEnabled = data?.enabled ?? false;
await this._setEverywhereState(isEnabled); // writes to chrome.storage.local
}// Fetches per-account feature flags. Requires login.
// Also tracked with metric event he.ff3p ('endpoint:ff3p').
async fetchFeatureFlags() {
if (!(await this._authService.isLoggedIn())) return;
const url = new URL("v1/user/ff3p?no_encode=true&inc1=true", this._apiService.BASE_URL);
const response = await fetch(url);
if (response.ok && response.status === HttpStatus.OK) {
const data = await response.json();
await this._saveFeatureFlags(data); // stored at 'feature-flags' in chrome.storage
}
}- plugin.rocketreach.co
Primary extension API. Serves the rr-everywhere toggle and per-account feature flags. Also the sole externally_connectable origin, so this host can message the extension directly.
RocketReach sends metrics as reversible base64
RocketReach posts a metrics batch to plugin.rocketreach.co/browser-extension/v1/m5s.
Evidence: the body decodes via base64 into JSON metrics; re-encoding reproduces it.
Built via btoa(JSON.stringify()): HTTPS-protected, no added encryption.
You browse while the extension is installed and enabled.
The extension batches activity metrics and sends them to RocketReach as reversible base64-encoded JSON.
| Field | Value | Why it matters | |
|---|---|---|---|
Metric type | increment | Shows what kind of measurement the extension is reporting about your extension activity. | |
Metric names | fetch; url_resolution | Shows which browser-extension events were recorded in the batch. | |
Result tags | status:success; status:no_match | Adds outcome details to the metric events, such as whether a fetch succeeded or a URL resolution matched. | |
Context tags | endpoint:user; page_type:generic | Adds page or endpoint context to the metric event so requests can be grouped by activity type. |
The extension waits about 10 seconds before sending the queued metrics batch.
The metric service serializes the batch and applies base64 encoding
!function(r) {
var i, o, s;
r.OnMessageName = "util.Metric";
class u extends re {
constructor() {
super(), i.set(this, void 0), o.set(this, void 0), s.set(this, void 0), this._batch = [], this._pendingBatchRequest = null, this._batchDelayMS = 1e3;
}
static build() {
return n(this, void 0, void 0, (function*() {
const e = new u;
return t(e, i, yield Ue.Service.build(), "f"), t(e, o, new URL("/browser-extension/v1/m5s", a(e, i, "f").BASE_URL), "f"), t(e, s, yield xe(), "f"), e
}))
}
increment(e, a) {
return n(this, void 0, void 0, (function*() {
this._enqueueMetric({
metric_type: $e.increment,
name: e,
tags: a
});
}))
}
histogram(e, a, t) {
return n(this, void 0, void 0, (function*() {
this._enqueueMetric({
metric_type: $e.histogram,
name: e,
value: a,
tags: t
});
}))
}
_enqueueMetric(e) {
return n(this, void 0, void 0, (function*() {
this._batch.push(e), this._ensureBatchEnqueued();
}))
}
_ensureBatchEnqueued() {
return n(this, void 0, void 0, (function*() {
if (this._pendingBatchRequest) return;
const e = this._getBatchDelaysMS();
this._pendingBatchRequest = setTimeout((() => {
this._sendBatch(), this._batch = [], clearTimeout(this._pendingBatchRequest), this._pendingBatchRequest = null;
}), e);
}))
}
_sendBatch() {
return n(this, void 0, void 0, (function*() {
yield fetch(a(this, o, "f"), {
method: "POST",
body: this.encodedBody()
});
}))
}
encodedBody() {
const e = JSON.stringify({
metrics: this._batch
});
return btoa(e)
}
_getBatchDelaysMS() {
return 1e4
}
}
i = new WeakMap, o = new WeakMap, s = new WeakMap, e([u.alwaysRunInServiceWorkerContext(r.OnMessageName)], u.prototype, "increment", null), e([u.alwaysRunInServiceWorkerContext(r.OnMessageName)], u.prototype, "histogram", null), r.Service = u;
}(Ee || (Ee = {}));Decodes a captured RocketReach metrics body from base64 and checks whether re-encoding the decoded text produces the same body.
const fs = require('fs');
const input = fs.readFileSync(0, 'utf8').trim();
if (!input) {
console.error('Usage: node rocketreach-metrics-base64-check.js < body.txt');
process.exit(1);
}
const decoded = Buffer.from(input, 'base64').toString('utf8');
console.log(decoded);
try {
const parsed = JSON.parse(decoded);
if (!Array.isArray(parsed.metrics)) {
console.error('Decoded JSON does not contain a metrics array.');
process.exit(2);
}
const roundTrip = Buffer.from(decoded, 'utf8').toString('base64');
console.error(`metrics entries: ${parsed.metrics.length}`);
console.error(`round-trip matches input: ${roundTrip === input}`);
} catch (err) {
console.error(`Decoded text is not valid JSON: ${err.message}`);
process.exit(3);
}
- 1Save a captured /browser-extension/v1/m5s request body to body.txt.
- 2Run: node rocketreach-metrics-base64-check.js < body.txt