Is JobOwl – AI Resume & ATS Cheat Code safe?

High risk

JobOwl sends the visible text from the active tab and its frames to api.jobowl.co when generating resumes or cover letters.

When a user asks JobOwl to generate an optimized resume or cover letter, the extension reads visible text from the active tab across its frames. It sends that page text, along with URL and frame details, to JobOwl's API endpoints even though the recorded claim says the intended job-board URL filter is not enforced.

MGolebiewski97v2.2.1Chrome Web Store
73Risk
Who publishes it

MGolebiewski97 - 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
MGolebiewski97
Registered address
Fabryczna 4A, 16, Warszawa 00-446, PL
Registered contact
Marek Gołębiewski

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

Publishers can request a review.

Findings

JobOwl sends active-tab text for resume generation

Using JobOwl's resume or cover-letter action reads visible text from the active tab, including frames, and sends it with the tab URL to JobOwl.

The source computes a job-board URL filter, but it goes unused before frames give up their text.

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

You ask the extension to generate a resume or cover letter.

This happens from the popup's optimized-resume or cover-letter action.

The extension did this

The extension reads visible text from the active tab and sends it to JobOwl with the tab URL.

The code computes a job-board URL filter but continues with the unfiltered frame list except for Google reCAPTCHA frames.

Fields assembled before the JobOwl API call
  • Visible page text
    Senior Product Manager - Remote. Responsibilities include roadmap planning, stakeholder interviews, and enterprise customer research.

    This can include the readable text on whatever active page you had open when you requested generated content.

  • Active-tab URL
    https://example.com/careers/senior-product-manager

    This identifies the page that supplied the text and can reveal the site or listing you were viewing.

  • Frame location
    origin=https://example.com, isTopFrame=true, frameId=https://example.com/careers/senior-product-manager

    This shows which page frame returned the text, including whether it was the top page or an embedded frame.

  • Timezone for cover letters
    America/New_York

    For cover-letter generation, the request also includes your browser's timezone.

Captured request
POSThttps://api.jobowl.co/api/supabase/optimized-resume
Headers
Content-Type
application/json
Captured request
POSThttps://api.jobowl.co/api/supabase/cover-letter
Headers
Content-Type
application/json
The code that does this

The frame reader, text collector, and API callers

Readable version

Readable equivalent of the frame enumeration

popup.100f6462.js
const fetchInnerText = ({ jobBoardsUrls }) => new Promise((resolve) => {  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {    const tabId = tabs[0]?.id;    if (!tabId) {      resolve(undefined);      return;    }    chrome.webNavigation.getAllFrames({ tabId }, (frames) => {      if (!frames || frames.length === 0) {        resolve(undefined);        return;      }      const responses = [];      let completed = 0;      const finish = () => {        const text = responses.reduce((combined, response) =>          response.frameInfo?.isTopFrame ? response.innerText + combined : combined + "\n" + response.innerText,          ""        );        resolve(text || undefined);      };      frames.filter((frame) => jobBoardsUrls.some((url) => frame.url.startsWith(url)));      const queriedFrames = frames.filter((frame) => !frame.url.startsWith("https://www.google.com/recaptcha/"));      queriedFrames.forEach((frame) => {        chrome.tabs.sendMessage(tabId, { type: "GET_INNER_TEXT" }, { frameId: frame.frameId }, (response) => {          completed++;          if (response && response.innerText !== undefined) responses.push(response);          if (completed === queriedFrames.length) finish();        });      });    });  });});

Readable equivalent of the content-script text collector

content.06af5f40.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {  if (message.type === "GET_INNER_TEXT") {    try {      const innerText = (() => {        let node;        const excludedTags = new Set(["script", "style", "noscript", "template", "svg", "canvas", "meta", "link", "head", "iframe"]);        const isVisible = (element) => {          let current = element;          while (current) {            const style = window.getComputedStyle(current);            if (current.hidden || style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;            current = current.parentElement;          }          return true;        };        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {          acceptNode: (textNode) => {            const parent = textNode.parentElement;            if (!parent || excludedTags.has(parent.tagName.toLowerCase())) return NodeFilter.FILTER_REJECT;            const value = textNode.nodeValue;            return value && value.trim() && isVisible(parent) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;          }        });        const values = [];        while ((node = walker.nextNode())) values.push((node.nodeValue ?? "").trim());        return values.join("\n").replace(/\n{3,}/g, "\n\n").trim();      })();      const frameInfo = getFrameInfo();      sendResponse({ innerText, frameInfo });    } catch (error) {      sendResponse({ innerText: "", frameInfo: getFrameInfo(), error: error instanceof Error ? error.message : String(error) });    }    return true;  }});

Readable equivalent of the optimized-resume POST

popup.100f6462.js
async function postOptimizedResume(payload) {  return authorizedFetch(`${JOBOWL_API_BASE_URL}/api/supabase/optimized-resume`, {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify(payload)  });}const useGenerateOptimizedResumeMutation = () => {  const userInfo = useQuery(userInfoQueryOptions).data;  const activeTabUrl = useActiveTab();  return useMutation({    mutationFn: async () => {      const description = await fetchInnerText({ jobBoardsUrls: userInfo?.jobBoardsUrls ?? [] });      if (!description || !activeTabUrl) throw Error("Unable to generate resume. Please refresh the current tab and try again.");      const response = await postOptimizedResume({ description, url: activeTabUrl });      if (!response.ok) throw Error("Optimised resume generation failed");    }  });};

Readable equivalent of the cover-letter POST

popup.100f6462.js
async function postCoverLetter(payload) {  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;  return authorizedFetch(`${JOBOWL_API_BASE_URL}/api/supabase/cover-letter`, {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify({ ...payload, timezone })  });}const useGenerateCoverLetterMutation = () => {  const activeTabUrl = useActiveTab();  const userInfo = useQuery(userInfoQueryOptions).data;  return useMutation({    mutationFn: async () => {      const description = await fetchInnerText({ jobBoardsUrls: userInfo?.jobBoardsUrls ?? [] });      if (!description || !activeTabUrl) throw Error("Unable to generate cover letter. Please refresh the current tab and try again.");      const response = await postCoverLetter({ description, url: activeTabUrl });      if (!response.ok) throw Error("Cover letter generation failed");    }  });};
External host receiving the generated-content input
    • api.jobowl.co

    Receives resume and cover-letter generation requests containing the collected page text and active-tab URL.

What it can do

Permissions this extension asks for, as declared in version 2.2.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every secure site you visit

    https://*/*

  • Store data in your browser

    storage

  • Act on the current tab, but only after you click the extension

    activeTab

  • See every page you navigate to, as you navigate to it

    webNavigation

Where it sends data

Destinations our analysis observed JobOwl – AI Resume & ATS Cheat Code contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • api.jobowl.co

    JobOwl – AI Resume & ATS Cheat Code sends data to api.jobowl.co. No other extension we have analysed sends data here.

Updated 30 September 2026kjdkghpllmecaifbeifjlomfbjojlphj