Is FrogHire.ai - AI Resume & Job Search, Autofill, H1B Checker safe?

Medium risk

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.…

FrogHire.aiv3.10.0Chrome Web Store
45Risk
Who publishes it

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.

Publisher
FrogHire.ai
Registered address
5224 Riverfront Drive, Pittsburgh, PA 15238, US
Registered contact
Andrew Hang Chen

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

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.

The extension did this

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.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://www.froghire.ai/api/v1/misc/ext_config?id=index&scope=siteconfig
Observed response contained configuration data including Glassdoor/glassdoor.com and Google/www.google.com patterns.
03EvidenceFIELD TABLE
Remote configuration fields that affect extension behavior
FieldValueWhy it matters
Configuration ID
GoogleSelects which named site or autofill rule set the extension should use.
Configuration version
4Lets the extension decide whether the remote rule set matches the version it expects.
Host pattern
www.google.comControls which websites the extension treats as supported job or application pages.
Path or search pattern
/search with udm=8Narrows the matching rule to specific pages or searches within a supported site.
Autofill rule scope
autofillconfigCan point the extension to form-handling rules for supported application sites.
04EvidenceSTORAGE DUMP
Unknown block kind: storage_dump

The 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."
}
05EvidenceCODE COMPARE
The code that does this

Remote site and autofill configuration loaders

What it actually does
Deobfuscated siteconfig loaderdeobfuscated/content-scripts/isolated.js
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
}
Deobfuscated autofillconfig loaderdeobfuscated/content-scripts/autofill.js
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)
}
06EvidenceTHIRD PARTY LIST
Configuration host
  • www.froghire.ai

    Provides extension configuration through /api/v1/misc/ext_config for site matching and autofill rule loading.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You use a FrogHire.ai feature that records activity telemetry.

The background message handler accepts telemetry events from extension code.

The extension did this

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.

02EvidenceFIELD TABLE
Fields constructed for the Google Analytics event
FieldValueWhy it matters
Persistent client ID
8f0a5f3d-7c3b-4b78-b42e-6d8d20b7f64cLets the analytics endpoint connect multiple extension events from the same browser over time.
FrogHire user ID
usr_7f4b6c2a91Can tie the event to a signed-in FrogHire account when the event includes one.
Session ID
1784059200Groups your extension activity into a browsing session.
Event name
autofill_toolbar_openedDescribes what you did in the extension.
Page or feature context
https://www.linkedin.com/jobs/search/?keywords=analystShows what page or extension feature was active when the event was recorded.
Extension version
3.6.1Adds the installed extension version to the activity event.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-EMPK2W307S&api_secret=<redacted>
The source code expects the POST result to expose an ok status.
Headers
Content-Typeapplication/json
04EvidenceSTORAGE DUMP
What's stored on your device

The extension keeps an analytics client ID across sessions and keeps a shorter-lived session record for grouping activity.

Locationchrome.storage local key fh:telemetry:client_id and session key fh:telemetry:session
Contents (JSON)
{
  "local:fh:telemetry:client_id": "8f0a5f3d-7c3b-4b78-b42e-6d8d20b7f64c",
  "session:fh:telemetry:session": {
    "session_id": 1784059200,
    "last_seen_at": 1784059200123
  }
}
05EvidenceCODE COMPARE
The code that does this

Telemetry request construction and message handling

What it actually does
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
}
06EvidenceTHIRD PARTY LIST
Host receiving telemetry events
  • 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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did 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 did this

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.

02EvidenceFIELD TABLE
Cookie and request fields used by the Voyager lookup
FieldValueWhy it matters
LinkedIn JSESSIONID value
ajax:8392746501827364512This token helps authorize LinkedIn requests made in your signed-in browser session.
CSRF request header
csrf-token: ajax:8392746501827364512Places the cookie-derived token into a request header for the typeahead API.
Lookup keyword
San FranciscoShows the company or location text being resolved by LinkedIn typeahead.
Lookup type
type=GEOShows whether the request is for a job location or company lookup.
03EvidenceNETWORK CAPTURE
Captured request
GEThttps://www.linkedin.com/voyager/api/voyagerSearchDashReusableTypeahead?decorationId=com.linkedin.voyager.dash.deco.search.typeahead.ReusableTypeaheadCollection-32&keywords=San%20Francisco&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))
The code expects a JSON response with elements used to populate geoId and location fields.
Headers
csrf-tokenajax:8392746501827364512
04EvidenceCODE COMPARE
The code that does this

LinkedIn cookie read and Voyager typeahead requests

What it actually does
Deobfuscated cookie readerdeobfuscated/content-scripts/isolated.js
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 ""
  }
}
Deobfuscated LinkedIn request builderdeobfuscated/content-scripts/isolated.js
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()}`
}
05EvidenceTHIRD PARTY LIST
Host receiving the lookup request
  • www.linkedin.com

    Receives LinkedIn Voyager typeahead requests containing a csrf-token header derived from the LinkedIn page cookie.

Updated 30 September 2026jabnaledogdghdbckajlnbipcdicinom