Is JobWizard: AI Autofill for Workday, Greenhouse & 500+ Job Sites safe?

High risk

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

JobWizardv1.1.38Chrome Web Store
78Risk
Who publishes it

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.

Publisher
JobWizard

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

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 did this

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.

Data used to build the LinkedIn jobs API request
  • LinkedIn session cookie
    ajax:1234567890123456789 (illustrative)

    This identifies your signed-in LinkedIn browser session and can be used to make LinkedIn requests in that session.

  • LinkedIn job ID
    3906262508 (illustrative)

    This identifies the job posting the extension is looking up while you are using the LinkedIn feature.

  • Authenticated request marker
    credentials included

    This tells LinkedIn to treat the API call as part of your signed-in browser session.

Captured request
GEThttps://www.linkedin.com/voyager/api/jobs/jobPostings/

Observed during dynamic analysis as a LinkedIn Voyager jobs API request with credentials included; no request body was recorded.

Headers
accept
application/vnd.linkedin.normalized+json+2.1
csrf-token
LinkedIn JSESSIONID value returned by the extension background
The code that does this

Shipped code path that reads JSESSIONID and reuses it in the LinkedIn request

Readable version

Background branch that returns the LinkedIn session cookie

deobfuscated/js/background.js
if ("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.js
getLinkedinJobPostings: 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)}
Network destination used by this code path
    • 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
What actually happens
You did this

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.

The extension did this

JobWizard's content script stops real form writes and submissions and fakes success on its own reporting messages instead of sending them.

The code that does this

Content script that checks for the function before running

Readable version
(() => {  "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,  });})();
What changes once the function is present
  • Form field writes
    input.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 submission
    form.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.

Dormant as shipped

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

The user starts the Gmail forwarding setup flow.

The extension did this

The extension navigates Gmail settings, adds a JobWizard forwarding address, retrieves the Google verification code from api.jobwizard.ai, and completes the verification step.

Captured request
GETapi.jobwizard.ai/email/forwarding-status
Fields used during Gmail forwarding setup
  • Gmail page content
    Forwarding 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 data
    user@example.com (illustrative)

    Stored user and email identifiers are used while requesting and checking the JobWizard forwarding address.

Observation

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

Updated 30 September 2026kbhgdbfkbgkokgkkdhnnlmkhnokjmfib