Is Jobright Autofill – Instant Job Applications, Job Match, AI Tailor Resume safe?
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.…
Who publishes itJobright - 1 other listing from the same operator, none carrying a finding
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.
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.
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
You save changed autofill information in Jobright Autofill.
The code validates required personal, education, work-experience, and equal-employment fields before saving.
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.
- Identity and contact detailsJane 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 linksgithub.com/janedoe, janedoe.dev (illustrative)
These URLs connect the saved application profile to your professional and personal web presence.
- Address and locationUnited 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 historyAcme 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 attributesgender, 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 salarysalary 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.
- Content-Type
- application/json
Saved autofill information flows from the UI into the background POST
The save button builds the body and sends saveAutofillInfo
deobfuscated/helper-app.41ea2652.jsti = 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.jsl = ({ 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- 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.
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
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 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.
- Job listing ID3901234567
Identifies the specific LinkedIn job you imported.
- Job titleSenior Backend Engineer
Shows which role you were viewing when you imported the listing.
- Full job descriptionBuild 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 detailscompany_id=1441, company_name=Example Systems
Names the employer tied to the listing and includes LinkedIn's company identifier when available.
- Listing contextSan Francisco, CA; is_expired=false
Adds where the job is located and whether LinkedIn marked the listing closed.
The shipped API helper parses the JSON response and returns the response result as the imported job ID.
- Content-Type
- application/json
The LinkedIn click path extracts page state and posts it to Jobright
Click-side import message
helper-app.41ea2652.jsasync 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.jsfunction 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.jsasync 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;}- 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
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 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.
- 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 limit200,000 characters
Allows a large portion of the current page to be included when the fallback runs.
- Iframe sourceshttps://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.
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.
- Content-Type
- application/json
The fallback sends cleaned page content for server-side parsing
Fallback condition on Add External Job form mount
helper-app.41ea2652.jsasync 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.jsfunction 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.jsasync 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.jsasync 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;}- api.jobright.ai
Receives Markdown derived from the current page at /swan/autofill/external-job.