Is Jobright Autofill – Instant Job Applications, Job Match, AI Tailor Resume safe?

High risk

Jobright is high risk. Saving changed autofill info sends Jobright's API contact details, address, education, work history, salary/hiring preferences, and equal-employment fields: gender, race, disability, veteran, work authorization, LGBT/Hispanic, pronouns.…

Jobrightv1.23.1Chrome Web Store
77Risk
Who publishes it

Jobright - 1 other listing from the same operator, none carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Jobright

Same store account

1 other listing published from this account, 29 users between them, none of them carrying a finding.

Shared hosts - 2 hostnames

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.

cs.ns1p.net
Also called by 2 other listings, including ReplyWise: AI Comment Generator
li.protechts.net
Also called by 3 other listings, including ReplyWise: AI Comment Generator

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

Publishers can request a review.

Findings

Jobright Sends Saved Profile and EEO Fields

Saving changed autofill info sends Jobright's API contact details, address, education, work history, salary/hiring preferences, and equal-employment fields: gender, race, disability, veteran, work authorization, LGBT/Hispanic, pronouns.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You save changed autofill information in Jobright Autofill.

The code validates required personal, education, work-experience, and equal-employment fields before saving.

The extension did this

The extension sends the assembled profile payload to Jobright's autofill-save API.

The background handler posts JSON to https://api.jobright.ai/swan/autofill/save with browser credentials included.

Fields assembled into the autofill-save payload
  • Identity and contact details
    Jane Doe, jane.doe@example.com, +1 415 555 0134, linkedin.com/in/janedoe (illustrative)

    This can identify you and link the saved application profile to your email, phone number, and profile URLs.

  • Profile links
    github.com/janedoe, janedoe.dev (illustrative)

    These URLs connect the saved application profile to your professional and personal web presence.

  • Address and location
    United States, California, San Francisco, 94105, 123 Market St (illustrative)

    This records where you live or are applying from, including postal-code-level detail.

  • Education and work history
    Acme Corp, Software Engineer, 2021-2024; Example University, B.S., 2017-2021 (illustrative)

    This describes your schools, employers, job titles, dates, summaries, and job descriptions.

  • Equal-employment attributes
    gender, race, disability, veteran status, work authorization, LGBT status, Hispanic status, sexual orientation, pronouns (field names from code)

    This can include protected or sensitive application attributes that many users expect to handle carefully.

  • Preferences and salary
    salary expectation, hiring date, additional application information (field names from code)

    This adds employment preferences and free-text application context to the same saved profile record.

Captured request
POSThttps://api.jobright.ai/swan/autofill/save
Headers
Content-Type
application/json
The code that does this

Saved autofill information flows from the UI into the background POST

Readable version

The save button builds the body and sends saveAutofillInfo

deobfuscated/helper-app.41ea2652.js
ti = async () => {  (0, H.trackEvent)("autofill_info_modal_update_click", {    activeSection: h  }), "signupInformation" === h && (0, H.trackEvent)("autofill_signup_information_update", {    uid: _.useProfileStore.getState().userStage?.userId  });  let e = tn();  if (e.size > 0) {    v(e);    let t = ["personal", "education", "workExperience", "equalEmployment"];    for (let r of t)      if ([...e].some(e => e.startsWith(r))) {        g(r);        break      } return  }  v(new Set);  let t = (0, N.getWorkdaySignupPasswordLocalUpdateAction)({      password: em,      isLoaded: eF,      isTouched: eP    }),    r = "save" === t,    n = (0, $.validateWorkdayPassword)(em);  if (r && !n.isValid) {    g("signupInformation"), eR(!0);    return  }  let o = (0, W.buildAutofillInfoSaveBody)(S),    a = eM.current ? (0, W.buildAutofillInfoSaveBody)(eM.current) : o,    l = {      ...o.structuredData,      regenerationEmail: void 0    },    s = {      ...a.structuredData,      regenerationEmail: void 0    },    u = JSON.stringify(l) !== JSON.stringify(s),    c = (0, T.buildSignupRegistrationEmailUpdateBody)(S.signupInformation.registrationEmail),    d = (eM.current?.signupInformation.registrationEmail ?? "").trim() !== S.signupInformation.registrationEmail.trim(),    f = eM.current?.preference.additionalApplicationInfo ?? "";  if (f !== S.preference.additionalApplicationInfo && (0, H.trackEvent)("autofill_custom_question_updated", {}), u) {    let e = (0, X.buildAutofillInfoTrackingSnapshot)(S),      t = eM.current ? (0, X.buildAutofillInfoTrackingSnapshot)(eM.current) : e;    (0, P.sendAutofillAnswerPairEvent)({      formUrl: (0, M.useUrlStore).getState().currentTabUrl,      autofillSnapshot: t.normal,      submitSnapshot: e.normal,      additionalAutofillData: {        education: t.education,        employment: t.employment      },      additionalSubmitData: {        education: e.education,        employment: e.employment      },      source: "apply"    })  }  try {    u ? await (0, k.sendToBackground)({      name: "saveAutofillInfo",      body: o    }) : await (0, k.sendToBackground)({      name: "updateAutofillSection",      body: c    })  } catch {    (0, i.message).error("Failed to update autofill information. Please try again.");    return  }  try {    let e = d;    r ? (await (0, N.saveWorkdaySignupInformation)({      password: em    }), e = !0) : "clear" === t && await (0, N.clearWorkdaySignupInformation)(), e && document.dispatchEvent(new CustomEvent(L.PRE_AUTOFILL_ACCOUNT_CREDENTIALS_CHANGED_EVENT))  } catch {    (0, i.message).error("Autofill information saved, but password was not saved.");    return  }(0, i.message).success("Autofill information updated."), eK(o.structuredData)}

The payload builder includes profile, resume, preference, and EEO fields

deobfuscated/helper-app.41ea2652.js
l = ({  personal: e,  education: t,  workExperience: r,  skill: n,  equalEmployment: i,  pronouns: l,  preference: s,  signupInformation: u}) => ({  structuredData: (0, o.withStructuredSkillList)({    regenerationEmail: u?.registrationEmail?.trim() ?? "",    personalInfo: {      firstName: e.firstName,      middleName: e.middleName,      lastName: e.lastName,      preferredFirstName: e.preferredFirstName,      preferredMiddleName: e.preferredMiddleName,      preferredLastName: e.preferredLastName,      email: e.email,      phone_number: e.phone,      linkedin: e.linkedinUrl,      github_url: e.githubUrl,      personal_site: e.websiteUrl    },    location: {      country: e.country,      state: e.state,      city: e.city,      county: e.county,      postCode: e.postalCode    },    state: e.state,    addressLine: e.addressLine,    phoneType: e.phoneType,    phoneCountryCode: (0, a.normalizePhoneCountryCode)(e.phoneCountryCode),    salary: s.salary,    hiringDate: s.hiringDate,    additionalApplicationInfo: s.additionalApplicationInfo,    education: t.map(e => ({      organization: e.schoolName,      accreditation: e.accreditation,      gpa: e.gap,      dates: {        start_date: e.startDate,        completion_date: "Present" === e.endDate ? null : e.endDate,        is_current: "Present" === e.endDate      }    })),    workExperience: r.map(e => ({      job_title: e.jobTitle,      organization: e.companyName,      location: e.city,      dates: {        start_date: e.startDate,        completion_date: "Present" === e.endDate ? null : e.endDate,        is_current: "Present" === e.endDate      },      summary: e.summary,      job_descriptions: e.descriptions    })),    employmentInfo: {      gender: i.gender,      race: i.ethnicity,      veteran: i.veteran,      disability: i.disability,      workAuthorization: i.workAuthorization,      sponsorshipStatus: i.sponsorshipStatus,      lgbt: i.lgbt,      hispanic: i.hispanic,      sexual: i.sexual    },    pronouns: l  }, n)})

The background handler and API client POST the JSON body

deobfuscated/static/background/index.js
// saveAutofillInfo handlerlet saveAutofillInfo = async (e, r) => {  let t = await (0, a.fetchSaveAutofillInfo)(e.body);  r.send(t)}// fetchSaveAutofillInfolet fetchSaveAutofillInfo = e => fetch(`${API_DOMAIN}/swan/autofill/save`, {  credentials: "include",  method: "POST",  headers: {    "Content-Type": "application/json"  },  body: JSON.stringify(e)}).then(e => e.json())// API domain resolverlet env = {  apiDomain: "https://api.jobright.ai",  hostDomain: "https://jobright.ai",  cookieDomain: ".jobright.ai"}let API_DOMAIN = env.apiDomain
Destination for the autofill-save request
    • api.jobright.ai

    Receives the structured autofill-save JSON payload for Jobright's service.

    • jobright.ai

    Hosts the Jobright account and extension workflow that must be signed in before this save path runs.

Traffic test limitation

Dynamic analysis did not observe the request because the save workflow required a signed-in Jobright account. The shipped code still shows the endpoint, request method, credential mode, JSON serialization, and the exact fields assembled before the background request runs.

LinkedIn job details sent to Jobright import API

Clicking Jobright's import banner on a LinkedIn listing runs a page script that reads the listing from LinkedIn's Ember store and sends the title, description, company ID/name, location, job state, and listing ID to Jobright's API.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You click the Jobright import banner while viewing a LinkedIn job listing.

This path requires both a LinkedIn login and the import click.

The extension did this

The extension reads the LinkedIn job record and submits it to Jobright for import.

The click handler, page-script extraction, and Jobright API POST path are present in the shipped source.

LinkedIn fields prepared for the import request
  • Job listing ID
    3901234567

    Identifies the specific LinkedIn job you imported.

  • Job title
    Senior Backend Engineer

    Shows which role you were viewing when you imported the listing.

  • Full job description
    Build distributed services for a hiring platform, partner with product teams, and improve application workflows.

    Includes the listing text that may describe duties, requirements, pay, benefits, and application instructions.

  • Company details
    company_id=1441, company_name=Example Systems

    Names the employer tied to the listing and includes LinkedIn's company identifier when available.

  • Listing context
    San Francisco, CA; is_expired=false

    Adds where the job is located and whether LinkedIn marked the listing closed.

Captured request
POSThttps://api.jobright.ai/swan/import/job

The shipped API helper parses the JSON response and returns the response result as the imported job ID.

Headers
Content-Type
application/json
The code that does this

The LinkedIn click path extracts page state and posts it to Jobright

Readable version

Click-side import message

helper-app.41ea2652.js
async function addExternalJob(jobRecord) {  trackEvent("autofill_external_job_parse_started", {    user_id: useProfileStore.getState().userStage?.userId,    source: checkSupportStatus() ? "supported_page" : "unsupported_page",    jobTitle: jobRecord.jobTitle,    companyName: jobRecord.companyName,    url: window.location.href  });  setAndBroadcastAnalyzeStatus("loading");  const jobId = await sendToBackground({    name: "postExternalJobImport",    body: jobRecord  });  if (jobId === HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR) {    setAndBroadcastAnalyzeStatus("error");  }  if (jobId) {    if (isLinkedinDomain(window.top.location.href)) {      sendToBackground({        name: "saveExternalJobId",        body: {          linkedinJobId: getCurrentJobId(),          externalJobId: jobId        }      });    }    setState({ jobId });  }  pollingExternalJob({    api: async () => sendToBackground({      name: "getExternalJobStatus",      body: { jobId }    }),    checkSuccess: status => EXTERNAL_JOB_IMPORT_STATUS.IMPORTED === status,    checkFailed: status => EXTERNAL_JOB_IMPORT_STATUS.FAILED === status,    onSuccess: async () => {      const bannerDetail = await sendToBackground({        name: "getJobBannerDetail",        body: { jobId }      });      syncExternalJobQueryToCurrentPage({ jobId: typeof jobId === "string" ? jobId : null });      setJobInfo(bannerDetail);      setFormValues(defaultFormValues);    }  });}

Main-world LinkedIn data extractor

static/background/index.js
function getPageLinkedinJobInfo() {  try {    const jobId = (() => {      const href = window.top.location.href;      const params = new URLSearchParams(window.top.location.search);      const currentJobId = params.get("currentJobId");      if (currentJobId) return currentJobId;      const match = href.match(/jobs\/view\/(\d+)/);      return match && match[1] ? match[1] : null;    })();    if (!jobId) {      console.error("[LinkedIn Job Extractor] Cannot get JobID");      return null;    }    const emberState = (() => {      try {        const Ember = window.requireModule ? window.requireModule("ember").default : window.Ember;        if (!Ember) {          console.error("[LinkedIn Job Extractor] Cannot find Ember module");          return null;        }        const app = Ember.Namespace.NAMESPACES.find(namespace => namespace instanceof Ember.Application);        if (!app || !app.__container__) {          console.error("[LinkedIn Job Extractor] Cannot find Ember application container");          return null;        }        const store = app.__container__.lookup("service:store");        if (!store || !store._globalM3RecordDataCache) {          console.error("[LinkedIn Job Extractor] Cannot find Ember data cache");          return null;        }        return { ember: Ember, cache: store._globalM3RecordDataCache };      } catch (error) {        console.error("[LinkedIn Job Extractor] Error getting Ember cache:", error);        return null;      }    })();    if (!emberState) {      console.error("[LinkedIn Job Extractor] Cannot get Ember cache");      return null;    }    return extractJobInfo(emberState.cache, jobId);  } catch (error) {    console.error("[LinkedIn Job Extractor] Error occurred during execution:", error);  }  function extractJobInfo(cache, jobId) {    try {      const baseKey = `urn:li:fsd_jobPosting:${jobId}`;      const cardKey = `urn:li:fsd_jobPostingCard:(${jobId},JOB_DETAILS)`;      const baseRecord = cache[baseKey];      if (!baseRecord || !baseRecord.__data) {        console.error("[LinkedIn Job Extractor] Cannot find job base data:", baseKey);        return { error: "Cannot find job base data", job_id: jobId };      }      const job = baseRecord.__data;      const cardRecord = cache[cardKey];      const card = cardRecord && cardRecord.__data || {};      if (!card || Object.keys(card).length === 0) {        console.error("[LinkedIn Job Extractor] Cannot find job card data:", cardKey);      }      const { company_id, company_name } = getCompanyInfo(cache, job);      return {        job_id: jobId,        job_title: job.title || "",        job_description: job.description && job.description.text || "",        company_id,        company_name,        location: getLocation(cache, job),        job_state: job.jobState || "",        is_expired: job.jobState === "CLOSED"      };    } catch (error) {      console.error("[LinkedIn Job Extractor] Error extracting job info:", error);      return { error: error.message, job_id: jobId, stack: error.stack };    }  }  function getCompanyInfo(cache, job) {    try {      const companyUrn = job.companyDetails && job.companyDetails.jobCompany && job.companyDetails.jobCompany["*company"];      if (!companyUrn) {        return {          company_id: null,          company_name: document.querySelector(".job-details-jobs-unified-top-card__company-name")?.textContent.trim() || ""        };      }      const parts = companyUrn.split(":");      const company_id = parts[parts.length - 1];      const companyRecord = cache[companyUrn] && cache[companyUrn].__data;      const company_name = companyRecord && companyRecord.name || "";      return { company_id, company_name };    } catch (error) {      console.warn("[LinkedIn Job Extractor] Getting company info failed:", error);      return { company_id: null, company_name: "" };    }  }  function getLocation(cache, job) {    try {      const locationUrn = job["*location"];      if (!locationUrn) return "";      const locationRecord = cache[locationUrn] && cache[locationUrn].__data;      return locationRecord && locationRecord.defaultLocalizedName || "";    } catch (error) {      console.warn("[LinkedIn Job Extractor] Getting location info failed:", error);      return "";    }  }}

Jobright import POST helper

static/background/index.js
async function importExternalJob(jobRecord) {  const response = await fetch(`${API_DOMAIN}/swan/import/job`, {    method: "POST",    headers: {      "Content-Type": "application/json"    },    body: JSON.stringify(jobRecord)  });  if (!response.ok) {    throw Error(`Failed to import job: ${response.statusText}`);  }  const json = await response.json();  return json?.result;}
Destination reached by the import path
    • api.jobright.ai

    Receives the imported LinkedIn job record at /swan/import/job.

Page HTML sent to Jobright when job parsing falls back

When Add External Job can't extract a full job record, the content script clones the body, strips scripts, styles, SVGs, iframes, other markup, keeps up to 200,000 chars of HTML, converts to Markdown, and posts it to Jobright's parsing API.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You open Add External Job on a page where the extension cannot locally find a complete job title, company, and description.

This path requires a signed-in Jobright extension state.

The extension did this

The extension prepares cleaned page content and sends Markdown derived from that page to Jobright for parsing.

The form mount, fallback condition, cleanup function, Markdown conversion, and API POST are present in the shipped source.

Fields prepared by the fallback parser path
  • Cleaned page HTML
    <main><h1>Senior Product Manager</h1><section>Responsibilities include roadmap planning, customer research, and launch coordination.</section></main>

    Includes visible page text that remains after scripts, styles, frames, metadata, and extension UI elements are removed.

  • HTML length limit
    200,000 characters

    Allows a large portion of the current page to be included when the fallback runs.

  • Iframe sources
    https://boards.greenhouse.io/embed/job_app?for=exampleco

    Keeps up to five large HTTP or HTTPS iframe URLs and may append fetched iframe body content to the parsed page text.

  • Markdown sent to Jobright
    # Senior Product Manager Responsibilities include roadmap planning and customer research.

    Transforms the cleaned HTML into text for server-side parsing of job title, company, and description.

Captured request
POSThttps://api.jobright.ai/swan/autofill/external-job

The API helper returns the response result, which the content script maps into job title, company name, job description, and company ID form fields when present.

Headers
Content-Type
application/json
The code that does this

The fallback sends cleaned page content for server-side parsing

Readable version

Fallback condition on Add External Job form mount

helper-app.41ea2652.js
async function runInitialExternalJobScan() {  const { formValues } = useExternalJobStore.getState();  const { data, ruleMatched } = scrapeJobPageData();  const updates = {};  if (formValues.url === "") {    updates.url = window.location.href;  }  if (ruleMatched) {    if (formValues.jobTitle === "" && data.jobTitle) updates.jobTitle = data.jobTitle;    if (formValues.companyName === "" && data.companyName) updates.companyName = data.companyName;    if (formValues.jobDescription === "" && data.jobDescription) updates.jobDescription = data.jobDescription;  }  if (Object.keys(updates).length > 0) {    setFormValues(updates);  }  const jobTitle = updates.jobTitle || formValues.jobTitle;  const companyName = updates.companyName || formValues.companyName;  const jobDescription = updates.jobDescription || formValues.jobDescription;  const missingRequiredField = !jobTitle || !companyName || !jobDescription;  const useFallback = !ruleMatched || missingRequiredField;  if (useFallback) {    await parsePageWithMarkdown();  }  const latest = useExternalJobStore.getState().formValues;  const hasCompleteJob = !!latest.jobTitle && !!latest.companyName && !!latest.jobDescription;  if (hasCompleteJob) {    trackEvent("autofill_external_job_source", {      source_type: useFallback ? "ai_scrape" : "frontend_scrape",      user_id: userStage?.userId,      current_url: window.location.href    });  }}

Clean and limit page HTML

contents.d42e7fcf.js
function extractCleanedHtml() {  try {    const body = document.body;    if (!body) {      return { html: "", iframeSrcs: [] };    }    const iframeSrcs = (() => {      const candidates = [];      document.querySelectorAll("iframe").forEach(iframe => {        const rawSrc = (iframe.getAttribute("src") || "").trim();        if (!rawSrc || blockedIframeProtocol.test(rawSrc)) return;        const area = (iframe.offsetWidth || 0) * (iframe.offsetHeight || 0);        if (area < 5000) return;        try {          const url = new URL(rawSrc, document.baseURI);          if (url.protocol !== "http:" && url.protocol !== "https:") return;          candidates.push({ src: url.toString(), area });        } catch {}      });      const largestAreaBySrc = new Map();      for (const { src, area } of candidates) {        const previous = largestAreaBySrc.get(src) ?? -1;        if (area > previous) largestAreaBySrc.set(src, area);      }      return Array.from(largestAreaBySrc.entries())        .sort((left, right) => right[1] - left[1])        .slice(0, 5)        .map(([src]) => src);    })();    const clone = body.cloneNode(true);    clone.querySelectorAll("[data-plasmo], plasmo-csui, [id^='plasmo']").forEach(node => node.remove());    clone.querySelectorAll("script, style, noscript, svg, iframe, link, meta").forEach(node => node.remove());    clone.querySelectorAll("*").forEach(node => {      node.removeAttribute("style");      node.removeAttribute("class");      Array.from(node.attributes).forEach(attribute => {        if (attribute.name.startsWith("data-")) {          node.removeAttribute(attribute.name);        }      });    });    const html = clone.innerHTML || "";    const limitedHtml = html.length > 200000 ? html.slice(0, 200000) : html;    return { html: limitedHtml, iframeSrcs };  } catch (error) {    console.error("[extractCleanedHtml] Error:", error);    return { html: "", iframeSrcs: [] };  }}

Background Markdown conversion and API call

static/background/index.js
async function parsePageMarkdownMessage(message, response) {  try {    const { html, iframeSrcs } = message.body;    const iframeHtml = await fetchIframeBodies(iframeSrcs || []);    const combinedHtml = iframeHtml ? `${html}${iframeHtml}` : html;    const markdown = (() => {      try {        const converted = NodeHtmlMarkdown.translate(combinedHtml);        return converted.length > 100000 ? converted.slice(0, 100000) : converted;      } catch (error) {        console.error("[htmlToMarkdown] Error:", error);        return "";      }    })();    if (!markdown) {      console.warn("[parsePageMarkdown] Empty markdown, skipping API call");      response.send(null);      return;    }    const parsed = await parsePageMarkdown({ web_markdown: markdown });    response.send(parsed);  } catch (error) {    console.error("Error parsing page markdown:", error);    response.send(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR);  }}

Jobright parsing endpoint helper

contents.d42e7fcf.js
async function parsePageMarkdown(payload) {  const response = await fetch(`${API_DOMAIN}/swan/autofill/external-job`, {    method: "POST",    headers: {      "Content-Type": "application/json"    },    body: JSON.stringify(payload)  });  if (!response.ok) {    throw Error(`Failed to parse page: ${response.statusText}`);  }  const json = await response.json();  return json?.result ?? null;}
Destination reached by the fallback parser
    • api.jobright.ai

    Receives Markdown derived from the current page at /swan/autofill/external-job.

Updated 30 September 2026odcnpipkhjegpefkfplmedhmkmmhmoko