Is JobWizard: AI Autofill for Workday, Greenhouse & 500+ Job Sites safe?
JobWizard is high risk. On a LinkedIn job page, JobWizard reads your JSESSIONID cookie and uses it as the csrf-token header on a GET to LinkedIn's Voyager jobs API, with credentials included, in your signed-in session.…
Who publishes itJobWizard - no other listings under this identity
JobWizard - 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
LinkedIn session cookie used in jobs API requests
On a LinkedIn job page, JobWizard reads your JSESSIONID cookie and uses it as the csrf-token header on a GET to LinkedIn's Voyager jobs API, with credentials included, in your signed-in session.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You use the extension's LinkedIn job feature while signed in to LinkedIn.
The relevant code path is tied to LinkedIn job-posting lookups.
The extension reads your LinkedIn session cookie and uses it in an authenticated jobs API request.
The request is a GET to LinkedIn's Voyager job-posting endpoint with credentials included.
- LinkedIn session cookieajax:1234567890123456789 (illustrative)
This identifies your signed-in LinkedIn browser session and can be used to make LinkedIn requests in that session.
- LinkedIn job ID3906262508 (illustrative)
This identifies the job posting the extension is looking up while you are using the LinkedIn feature.
- Authenticated request markercredentials included
This tells LinkedIn to treat the API call as part of your signed-in browser session.
Observed during dynamic analysis as a LinkedIn Voyager jobs API request with credentials included; no request body was recorded.
- accept
- application/vnd.linkedin.normalized+json+2.1
- csrf-token
- LinkedIn JSESSIONID value returned by the extension background
Shipped code path that reads JSESSIONID and reuses it in the LinkedIn request
Background branch that returns the LinkedIn session cookie
deobfuscated/js/background.jsif ("getCookie" === m.method) return chrome.cookies.getAll({ url: "https://www.linkedin.com", name: "JSESSIONID"}).then(e => l(e?.[0]?.value)).catch(e => { try { l(null) } catch (e) {}}), !0;Content-script helper that sends the authenticated jobs API request
deobfuscated/js/bundle.jsgetLinkedinJobPostings: async e => { var t = await chrome.runtime.sendMessage({ method: "getCookie", url: "https://www.linkedin.com", name: "JSESSIONID" }) || ''; return fetch("https://www.linkedin.com/voyager/api/jobs/jobPostings/" + e, { method: "GET", credentials: "include", headers: { "csrf-token": String(t).replace(/"/g, ""), accept: "application/vnd.linkedin.normalized+json+2.1" } }).then(e => e.json()).then(e => e)}- www.linkedin.com
Receives the Voyager jobs API GET request with credentials included and a csrf-token header derived from the LinkedIn JSESSIONID cookie.
JobWizard script disables autofill logging and fakes success when a hook exists
A content script added in v1.1.18 checks every page for one specific global function before running.
If present, it blocks real form writes and submissions, and fakes success on its own reporting messages instead of sending them.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-506
- Source
- Dynamic sandbox
One specific global function is defined on the page before JobWizard's own script runs.
This does not happen during ordinary browsing; another script would need to define it first.
JobWizard's content script stops real form writes and submissions and fakes success on its own reporting messages instead of sending them.
Content script that checks for the function before running
(() => { "use strict"; const bindingName = "__jwGovernedEvaluationV4AttemptSink"; const sink = globalThis[bindingName]; if (typeof sink !== "function") return; const stateKey = "__JW_AUTOFILL_V4_TRIPWIRE__"; if (globalThis[stateKey]?.installed) return; const state = { installed: true, guardVersion: "autofill-v4-guard/1", nodeBindingInstalled: true, documentStartPreload: true, autofillAttempts: 0, uinfAttempts: 0, uploadAttempts: 0, formWriteAttempts: 0, submitAttempts: 0, networkMutationAttempts: 0, }; Object.defineProperty(globalThis, stateKey, { value: state, configurable: false }); const increment = (kind) => { state[kind] = Math.min(Number.MAX_SAFE_INTEGER, Number(state[kind] || 0) + 1); try { void sink(JSON.stringify({ version: state.guardVersion, kind })); } catch (_) {} }; const wrapSetter = (prototype, property) => { const descriptor = Object.getOwnPropertyDescriptor(prototype, property); if (!descriptor?.set || descriptor.set.__jwGovernedEvaluationV4) return; const guarded = function governedEvaluationV4Setter(value) { increment(property === "files" ? "uploadAttempts" : "formWriteAttempts"); return value; }; Object.defineProperty(guarded, "__jwGovernedEvaluationV4", { value: true }); Object.defineProperty(prototype, property, { ...descriptor, set: guarded }); }; [ [HTMLInputElement.prototype, "value"], [HTMLInputElement.prototype, "checked"], [HTMLInputElement.prototype, "files"], [HTMLTextAreaElement.prototype, "value"], [HTMLSelectElement.prototype, "value"], [HTMLSelectElement.prototype, "selectedIndex"], ].forEach(([prototype, property]) => { try { wrapSetter(prototype, property); } catch (_) {} }); try { const nativeSubmit = HTMLFormElement.prototype.submit; HTMLFormElement.prototype.submit = function governedEvaluationV4Submit() { increment("submitAttempts"); return undefined; }; Object.defineProperty(HTMLFormElement.prototype.submit, "name", { value: nativeSubmit.name }); const nativeRequestSubmit = HTMLFormElement.prototype.requestSubmit; if (nativeRequestSubmit) { HTMLFormElement.prototype.requestSubmit = function governedEvaluationV4RequestSubmit() { increment("submitAttempts"); return undefined; }; Object.defineProperty(HTMLFormElement.prototype.requestSubmit, "name", { value: nativeRequestSubmit.name }); } document.addEventListener("submit", (event) => { event.preventDefault(); event.stopImmediatePropagation(); increment("submitAttempts"); }, true); } catch (_) {} try { const runtime = globalThis.chrome?.runtime; const nativeSendMessage = runtime?.sendMessage?.bind(runtime); if (nativeSendMessage) { runtime.sendMessage = function governedEvaluationV4SendMessage(...args) { const message = args.find((entry) => entry && typeof entry === "object" && !Array.isArray(entry)) || {}; const requestPath = String(message.path || "").replace(/^\/+/, ""); const isUinf = requestPath === "uinf"; const isUpload = /(?:^|\/)upload_resume(?:$|[/?])/u.test(requestPath); const isUserWrite = /^(?:save_apply_result|activity\/batch|bug|profile\/(?:upload_resume|delete_resume|resume_set_default|save)|save_ats_profile|save_profile_)/u.test(requestPath); if (isUinf || isUpload || isUserWrite) { increment("networkMutationAttempts"); if (isUinf) increment("uinfAttempts"); if (isUpload) increment("uploadAttempts"); const callback = args.find((entry) => typeof entry === "function"); if (callback) queueMicrotask(() => callback({ status: -1, blocked_by_runner: true })); return Promise.resolve({ status: -1, blocked_by_runner: true }); } return nativeSendMessage(...args); }; } } catch (_) {} document.addEventListener("click", (event) => { const target = event.target?.closest?.("button,[role='button'],input"); if (!target) return; const text = String(target.textContent || target.value || target.getAttribute?.("aria-label") || ""); const upload = target instanceof HTMLInputElement && target.type === "file"; const autofill = /autofill|auto[ -]?fill/i.test(text); if (autofill) increment("autofillAttempts"); if (upload) increment("uploadAttempts"); if (autofill || upload) { event.preventDefault(); event.stopImmediatePropagation(); } }, true); Object.defineProperty(globalThis, "__JW_GOVERNED_EVALUATION_V4_GUARD__", { value: Object.freeze({ blockAutofill() { increment("autofillAttempts"); return true; }, }), configurable: false, });})();- Form field writesinput.value write is discarded without updating the field (illustrative)
Typing or autofill values stop reaching the page's actual input, select, and textarea fields.
- Form submissionform.submit() returns without effect (illustrative)
Attempts to submit the form, by button or key press, are blocked before they reach the page.
- Extension reports{status: -1, blocked_by_runner: true} (illustrative)
Messages the script would send to JobWizard's bug-report, upload, and profile-save endpoints get a fake success reply instead of sending.
As shipped in this version, no other file defines the specific function this script checks for, so the guard does not run its behavior during normal use. It would activate only if something else on the page defined that function first.
Gmail forwarding setup routes incoming mail to JobWizard servers.
Using the JobWizard Gmail setup flow, the extension navigates Gmail settings, adds a forwarding address at apply.jobwizard.ai, and enters the code from api.jobwizard.ai.
Incoming Gmail can then be forwarded to JobWizard's servers.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
The user starts the Gmail forwarding setup flow.
The extension navigates Gmail settings, adds a JobWizard forwarding address, retrieves the Google verification code from api.jobwizard.ai, and completes the verification step.
- Gmail page contentForwarding and POP/IMAP settings dialog (illustrative)
Gmail settings page elements are read and manipulated so the extension can add and verify the forwarding address.
- Stored account datauser@example.com (illustrative)
Stored user and email identifiers are used while requesting and checking the JobWizard forwarding address.
Dynamic analysis confirmed the content script is injected broadly and that the Gmail setup flow fetches the forwarding address, fills it into Gmail, polls the forwarding-status endpoint, and enters the returned verification code.
+1 more finding not shown