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.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
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.
Observed response contained configuration data including Glassdoor/glassdoor.com and Google/www.google.com patterns.
- Configuration IDGoogle
Selects which named site or autofill rule set the extension should use.
- Configuration version4
Lets the extension decide whether the remote rule set matches the version it expects.
- Host patternwww.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 scopeautofillconfig
Can point the extension to form-handling rules for supported application sites.
The bundled index shows the kind of host and URL patterns the extension uses to decide when job-search features apply.
- Location
- Bundled siteconfig/index.json
Remote site and autofill configuration loaders
Deobfuscated siteconfig loader
deobfuscated/content-scripts/isolated.jsfunction 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}Deobfuscated autofillconfig loader
deobfuscated/content-scripts/autofill.jsfunction 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.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
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.
- Persistent client ID8f0a5f3d-7c3b-4b78-b42e-6d8d20b7f64c
Lets the analytics endpoint connect multiple extension events from the same browser over time.
- FrogHire user IDusr_7f4b6c2a91
Can tie the event to a signed-in FrogHire account when the event includes one.
- Session ID1784059200
Groups your extension activity into a browsing session.
- Event nameautofill_toolbar_opened
Describes what you did in the extension.
- Page or feature contexthttps://www.linkedin.com/jobs/search/?keywords=analyst
Shows what page or extension feature was active when the event was recorded.
- Extension version3.6.1
Adds the installed extension version to the activity event.
The source code expects the POST result to expose an ok status.
- Content-Type
- application/json
The extension keeps an analytics client ID across sessions and keeps a shorter-lived session record for grouping activity.
- Location
- 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.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
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.
- LinkedIn JSESSIONID valueajax:8392746501827364512
This token helps authorize LinkedIn requests made in your signed-in browser session.
- CSRF request headercsrf-token: ajax:8392746501827364512
Places the cookie-derived token into a request header for the typeahead API.
- Lookup keywordSan Francisco
Shows the company or location text being resolved by LinkedIn typeahead.
- Lookup typetype=GEO
Shows whether the request is for a job location or company lookup.
The code expects a JSON response with elements used to populate geoId and location fields.
- csrf-token
- ajax:8392746501827364512
LinkedIn cookie read and Voyager typeahead requests
Deobfuscated cookie reader
deobfuscated/content-scripts/isolated.jsasync 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 "" }}Deobfuscated LinkedIn request builder
deobfuscated/content-scripts/isolated.jsasync 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.