Is FrogHire.ai - AI Resume & Job Search, Autofill, H1B Checker safe?
FrogHire.ai is medium risk. Dynamic analysis observed the extension requesting FrogHire.ai remote config; the response contained site patterns including Glassdoor and Google. The shipped code caches this, letting the service set site list, selectors, and autofill.…
Who publishes itFrogHire.ai - no other listings under this identity
FrogHire.ai - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
FrogHire remote configuration controls site matching
Dynamic analysis observed the extension requesting FrogHire.ai remote config; the response contained site patterns including Glassdoor and Google.
The shipped code caches this, letting the service set site list, selectors, and autofill.
The extension starts or reaches a page that needs site or autofill rules.
The code checks bundled files and cached entries, then can request configuration from FrogHire.ai.
FrogHire.ai can return the matching patterns and rule versions used by the extension.
Dynamic analysis observed site configuration data for Glassdoor and Google being returned.
| Field | Value | Why it matters | |
|---|---|---|---|
Configuration ID | Selects which named site or autofill rule set the extension should use. | ||
Configuration version | 4 | Lets the extension decide whether the remote rule set matches the version it expects. | |
Host pattern | www.google.com | Controls which websites the extension treats as supported job or application pages. | |
Path or search pattern | /search with udm=8 | Narrows the matching rule to specific pages or searches within a supported site. | |
Autofill rule scope | autofillconfig | Can point the extension to form-handling rules for supported application sites. |
storage_dumpThe data has shipped a block kind this view doesn't render yet. Raw payload below.
{
"kind": "storage_dump",
"location": "Bundled siteconfig/index.json",
"contents": "",
"meaning": "The bundled index shows the kind of host and URL patterns the extension uses to decide when job-search features apply.",
"technicalNote": "These entries are from the shipped siteconfig/index.json. The runtime loader can replace index or per-site entries with data returned by getExtConfig when cache or bundle lookup does not satisfy the requested version."
}Remote site and autofill configuration loaders
function AV(t) {
const {
loadBundle: e,
dbNamespace: n,
dbId: i = "0",
cacheTime: a,
fetchUrl: r = "",
fetchQuery: s = {}
} = t;
async function o() {
try {
return await on.background.idb({
method: "get",
namespace: n,
id: i
}) || null
} catch {
return null
}
}
async function l() {
if (!r) return null;
try {
const {
data: c
} = await on.background.fetch({
url: r,
query: s
});
return c ? (on.background.idb({
method: "set",
namespace: n,
id: i,
value: c,
cacheTime: a
}).catch(console.error), c) : null
} catch {
return null
}
}
return async function() {
return await o() || await l() || []
}
}
function gV(t) {
const {
loadBundle: e,
dbNamespace: n,
fetchUrl: i = ""
} = t;
async function a(s, o) {
try {
const l = await on.background.idb({
method: "get",
namespace: n,
id: s
});
return l && l.version === o ? l : null
} catch {
return null
}
}
async function r(s, o) {
if (!i) return null;
try {
const {
data: l
} = await on.background.fetch({
url: i,
query: {
id: s,
scope: o
}
});
return l && on.background.idb({
method: "set",
namespace: n,
id: s,
value: l
}).catch(console.error), l || null
} catch {
return null
}
}
return async function(o, l, c) {
return await e(o, l) || await a(o, l) || await r(o, c)
}
}
const fV = "siteconfig",
she = 3600 * 1e3,
lhe = "0",
ohe = "content-siteindex",
Ahe = "content-siteconfig";
let TY = null;
const mV = new Map,
DY = new Map,
pV = new WeakMap,
che = lV(fV),
uhe = oV(fV),
dhe = gV({
loadBundle: che,
dbNamespace: Ahe,
fetchUrl: kn.getExtConfig
}),
hhe = AV({
loadBundle: uhe,
dbNamespace: ohe,
dbId: lhe,
cacheTime: she,
fetchUrl: kn.getExtConfig,
fetchQuery: {
id: "index",
scope: "siteconfig"
}
});
function ghe(t) {
return `${t.id}:${t.version}`
}
async function fhe() {
return TY || (TY = hhe().then(uV)), await TY
}
async function mhe(t) {
const e = ghe(t),
n = mV.get(e);
if (n) return n;
const i = DY.get(e);
if (i) return await i;
const a = dhe(t.id, t.version, "siteconfig").then(r => (r && mV.set(e, r), r)).finally(() => {
DY.delete(e)
});
return DY.set(e, a), await a
}
async function phe(t) {
const e = await fhe(),
n = hV(t, e);
if (!n) return null;
const i = await mhe(n);
return i ? {
meta: n,
config: i
} : null
}function Ba(t) {
const {
dbNamespace: e,
dbId: r = "0",
cacheTime: n,
fetchUrl: a = "",
fetchQuery: s = {}
} = t;
async function o() {
try {
return await G.background.idb({
method: "get",
namespace: e,
id: r
}) || null
} catch {
return null
}
}
async function i() {
if (!a) return null;
try {
const {
data: c
} = await G.background.fetch({
url: a,
query: s
});
return c ? (G.background.idb({
method: "set",
namespace: e,
id: r,
value: c,
cacheTime: n
}).catch(console.error), c) : null
} catch {
return null
}
}
return async function() {
return await o() || await i() || []
}
}
function Va(t) {
const {
loadBundle: e,
dbNamespace: r,
fetchUrl: n = ""
} = t;
async function a(o, i) {
try {
const c = await G.background.idb({
method: "get",
namespace: r,
id: o
});
return c && c.version === i ? c : null
} catch {
return null
}
}
async function s(o, i) {
if (!n) return null;
try {
const {
data: c
} = await G.background.fetch({
url: n,
query: {
id: o,
scope: i
}
});
return c && G.background.idb({
method: "set",
namespace: r,
id: o,
value: c
}).catch(console.error), c || null
} catch {
return null
}
}
return async function(i, c, l) {
return await e(i, c) || await a(i, c) || await s(i, l)
}
}
const za = "autofillconfig",
Qa = 720 * 60 * 1e3,
Xa = "0",
Ka = "content-autofillindex",
Za = "content-autofillconfig";
let gt = null;
const Ja = qa(za),
es = Va({
loadBundle: Ja,
dbNamespace: Za,
fetchUrl: be.getExtConfig
}),
ts = Ba({
dbNamespace: Ka,
dbId: Xa,
cacheTime: Qa,
fetchUrl: be.getExtConfig,
fetchQuery: {
id: "index",
scope: "autofillconfig"
}
});
async function rs() {
return gt || (gt = ts().then(Wa)), await gt
}
async function ns(t) {
const e = await rs();
return Ya(t, e)
}- www.froghire.ai
Provides extension configuration through /api/v1/misc/ext_config for site matching and autofill rule loading.
Google Analytics receives FrogHire activity identifiers
The extension posts FrogHire.ai activity events to Google Analytics on telemetry.
The URL carries the measurement ID and hardcoded API secret; the body carries client ID, user ID, session ID, event name, version, parameters.
You use a FrogHire.ai feature that records activity telemetry.
The background message handler accepts telemetry events from extension code.
The extension packages that activity with persistent identifiers and posts it to Google Analytics.
The request includes a stored client ID, optional FrogHire user ID, session ID, event name, and extension version.
| Field | Value | Why it matters | |
|---|---|---|---|
Persistent client ID | 8f0a5f3d-7c3b-4b78-b42e-6d8d20b7f64c | Lets the analytics endpoint connect multiple extension events from the same browser over time. | |
FrogHire user ID | usr_7f4b6c2a91 | Can tie the event to a signed-in FrogHire account when the event includes one. | |
Session ID | 1784059200 | Groups your extension activity into a browsing session. | |
Event name | autofill_toolbar_opened | Describes what you did in the extension. | |
Page or feature context | https://www.linkedin.com/jobs/search/?keywords=analyst | Shows what page or extension feature was active when the event was recorded. | |
Extension version | 3.6.1 | Adds the installed extension version to the activity event. |
| Content-Type | application/json |
The extension keeps an analytics client ID across sessions and keeps a shorter-lived session record for grouping activity.
chrome.storage local key fh:telemetry:client_id and session key fh:telemetry:session{
"local:fh:telemetry:client_id": "8f0a5f3d-7c3b-4b78-b42e-6d8d20b7f64c",
"session:fh:telemetry:session": {
"session_id": 1784059200,
"last_seen_at": 1784059200123
}
}Telemetry request construction and message handling
const Tr = "G-EMPK2W307S",
_r = "<redacted>",
Sr = "https://www.google-analytics.com",
kr = 1800 * 1e3,
ut = te.defineItem("local:fh:telemetry:client_id", {
fallback: ""
}),
xe = te.defineItem("session:fh:telemetry:session", {
fallback: null
});
async function Pr(e) {
const t = await Ir(),
r = await Cr(),
n = "/mp/collect",
s = `${Sr}${n}?measurement_id=${encodeURIComponent(Tr)}&api_secret=${encodeURIComponent(_r)}`,
o = await fetch(s, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: t,
user_id: e.user_id,
consent: {
ad_user_data: "DENIED",
ad_personalization: "DENIED"
},
events: e.events.slice(0, 25).map(i => ({
name: i.name,
params: {
...i.params,
session_id: r,
engagement_time_msec: 100
}
}))
})
});
if (n.startsWith("/debug")) {
const i = await o.json().catch(() => null);
i?.validationMessages?.length && console.warn("[telemetry] GA validation messages", i.validationMessages)
}
return {
ok: o.ok
}
}
async function Ir() {
const e = await ut.getValue();
if (e) return e;
const t = globalThis.crypto.randomUUID?.() || `${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}`;
return await ut.setValue(t), t
}
async function Cr() {
const e = Date.now(),
t = await xe.getValue();
if (t && e - t.last_seen_at < kr) return await xe.setValue({
...t,
last_seen_at: e
}), t.session_id;
const r = {
session_id: Math.floor(e / 1e3),
last_seen_at: e
};
return await xe.setValue(r), r.session_id
}
function Rr() {
E.runtime.onMessage.addListener(e => {
xr(e) && jr(e).catch(() => {})
})
}
async function jr(e) {
const {
user_id: t,
...r
} = e.props, n = {
name: e.key,
params: Or({
...r,
app_version: E.runtime.getManifest().version
})
};
await Pr({
user_id: typeof t == "string" ? t : void 0,
events: [n]
})
}
function xr(e) {
if (!e || typeof e != "object") return !1;
const t = e;
return t.type === hr && typeof t.key == "string" && !!t.props && typeof t.props == "object" && !Array.isArray(t.props)
}
function Or(e) {
const t = {};
for (const [r, n] of Object.entries(e)) {
if (typeof n == "boolean") {
t[r] = n;
continue
}
if (typeof n == "number" && Number.isFinite(n)) {
t[r] = n;
continue
}
if (typeof n == "string") {
const s = n.trim();
s && (t[r] = s.slice(0, 100))
}
}
return t
}- www.google-analytics.com
Receives Google Analytics Measurement Protocol events with the extension's client ID, optional user ID, session ID, event name, and event parameters.
LinkedIn CSRF token is used in Voyager lookups
The extension reads LinkedIn cookies building FrogHire.ai search URLs.
For a location or company lookup, it puts JSESSIONID from the cookie into the csrf-token header on Voyager typeahead GETs.
A test skipping LinkedIn did not trigger this.
You use a FrogHire.ai LinkedIn search workflow with a location or company value.
The lookup is reached when the extension needs LinkedIn typeahead data to build the search URL.
The extension reads LinkedIn cookies and sends a cookie-derived CSRF token to LinkedIn Voyager.
The request stays on www.linkedin.com but uses your page session token in an extension-initiated request.
| Field | Value | Why it matters | |
|---|---|---|---|
LinkedIn JSESSIONID value | ajax:8392746501827364512 | This token helps authorize LinkedIn requests made in your signed-in browser session. | |
CSRF request header | csrf-token: ajax:8392746501827364512 | Places the cookie-derived token into a request header for the typeahead API. | |
Lookup keyword | San Francisco | Shows the company or location text being resolved by LinkedIn typeahead. | |
Lookup type | type=GEO | Shows whether the request is for a job location or company lookup. |
| csrf-token | ajax:8392746501827364512 |
LinkedIn cookie read and Voyager typeahead requests
async function SXe(t) {
if (typeof document < "u") return document.cookie || "";
const e = X5[t];
if (!e) return "";
try {
const { hostname: n } = new URL(e), i = await bs.cookies.getAll({ domain: n });
return !Array.isArray(i) || i.length <= 0 ? "" : i.map(a => `${a.name}=${a.value}`).join("; ")
} catch {
return ""
}
}async function RXe(t) {
(t.job_title || t.custom_keyword) && (t.job_title = [t.job_title, t.custom_keyword].join(" ").trim());
const e = { keywords: t.job_title || "jobs", refresh: "true" }, n = await SXe(Ti.LinkedIn);
if (t.job_location)
if (t.job_location === qB) e.geoId = "103644278", e.location = "United States", e.f_WT = "2";
else {
t.job_location === "Washington, DC" && (t.job_location = "Washington DC-Baltimore Area");
const o = encodeURIComponent(t.job_location.split(",")[0].trim() || t.job_location);
try {
const l = await fetch(`https://www.linkedin.com/voyager/api/voyagerSearchDashReusableTypeahead?decorationId=com.linkedin.voyager.dash.deco.search.typeahead.ReusableTypeaheadCollection-32&keywords=${o}&q=type&query=(typeaheadUseCase:JOBS)&type=GEO&typeaheadFilterQuery=(geoSearchTypes:List(POSTCODE_1,POSTCODE_2,POPULATED_PLACE,ADMIN_DIVISION_1,ADMIN_DIVISION_2,COUNTRY_REGION,MARKET_AREA,COUNTRY_CLUSTER))`, { method: "GET", headers: { "csrf-token": n.match(/(^|;) *JSESSIONID="([^;]*)"/)?.[2] || "" } }).then(c => c.json());
if (l.elements && Array.isArray(l.elements) && l.elements.length > 0 && typeof l.elements[0] == "object") {
const c = l.elements[0];
e.geoId = c.trackingUrn?.replace("urn:li:geo:", ""), e.location = c.title?.text
}
} catch {}
}
if (t.company_name) try {
const o = await fetch(`https://www.linkedin.com/voyager/api/voyagerSearchDashReusableTypeahead?decorationId=com.linkedin.voyager.dash.deco.search.typeahead.ReusableTypeaheadCollection-32&keywords=${encodeURIComponent(t.company_name)}&q=type&query=(typeaheadUseCase:COMPANIES)&type=COMPANY`, { method: "GET", headers: { "csrf-token": n.match(/(^|;) *JSESSIONID="([^;]*)"/)?.[2] || "" } }).then(l => l.json());
o.elements && Array.isArray(o.elements) && o.elements.length > 0 && typeof o.elements[0] == "object" && (e.f_C = o.elements[0].trackingUrn?.replace("urn:li:organization:", ""))
} catch {}
if (t.job_level && ig.linkedin[t.job_level] && (e.f_E = String(ig.linkedin[t.job_level])), t.job_level && t.job_level === "Internship" && (e.keywords = `Internship ${e.keywords}`), t.date_posted && xc.linkedin[t.date_posted]) e.f_TPR = String(xc.linkedin[t.date_posted]);
else if (t.date_posted && typeof t.date_posted == "string") {
const o = t.date_posted.match(/(\d+)/)?.[1];
o && (e.f_TPR = `r${Number(o)*3600}`)
}
const i = WXe(Ti.LinkedIn);
let a = "https://www.linkedin.com", r = "/jobs/search/";
try { const o = new URL(i); a = o.origin, r = o.pathname } catch {}
return ["/jobs/search/", "/jobs/search-results/"].some(o => r.includes(o)) || (r = "/jobs/search/"), `${a+r}?${new URLSearchParams(e).toString()}`
}- www.linkedin.com
Receives LinkedIn Voyager typeahead requests containing a csrf-token header derived from the LinkedIn page cookie.