Is Teal - Job Search Companion safe?

Medium risk

Teal is medium risk. The extension fetches a config from S3 (teal.extension/config.v4.json) specifying which elements to read on 60+ job sites, including LinkedIn, Indeed, Glassdoor. It lives server-side, so scraped sites/fields can change without a new review.

www.tealhq.comv4.0.8Chrome Web Store
45Risk
Who publishes it

Teal Labs, Inc - 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
www.tealhq.com
Declared legal entity
Teal Labs, Inc
Registered address
7800 SW 57th Ave, South Miami, FL 33143-5528, US
Registered contact
David Fano

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Remote server controls what job-page data the extension scrapes

The extension fetches a config from S3 (teal.extension/config.v4.json) specifying which elements to read on 60+ job sites, including LinkedIn, Indeed, Glassdoor.

It lives server-side, so scraped sites/fields can change without a new review.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open a job listing page on a site like LinkedIn, Indeed, or Glassdoor.

The extension's content script runs automatically on the page.

The extension did this

The extension fetches a remote configuration file that tells it, per site, which page elements to read for the job title, company, description, and compensation.

The rules that decide what gets read live on the vendor's server, not in the reviewed extension code.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://s3.amazonaws.com/teal.extension/config.v4.json
200 OK. Observed during dynamic analysis: a fresh GET issued from the content script returned a ~34 KB JSON body containing a `scraper` object (CSS-selector rules keyed by 61+ job-site domains), a `mutationObservers` object, a `jobAppLikeness` heuristics object used to detect job-application pages, and an `ignore` list. An independent fetch of the same public URL returned an equivalent structure: 34,691 bytes, 126 site-path rules across 61 unique domains.
03EvidenceCODE COMPARE
The code that does this

content-scripts/content.js, fetching and caching the remote selector rules

What it actually does
Fetch + 3-hour cachecontent-scripts/content.js
const fetchRemoteConfig = async () =>
  environment.value.config
    ? (await fetch(environment.value.config)).json()
    : hardcodedFallbackConfig;

// webext-storage-cache wrapper: refetches config.v4.json at most every
// 3 hours, with a 1-hour stale-while-revalidate window.
const configCache = new StorageCache("config", {
  maxAge: { hours: 3 },
  staleWhileRevalidate: { hours: 1 },
  updater: fetchRemoteConfig,
});

// Every scraping call site goes through this to get the current rules.
const getScraperConfig = () => configCache.get();
Using the config to decide whether the current page is a job postingcontent-scripts/content.js
// True if the current URL/DOM matches ANY server-supplied site rule.
const isSupportedPage = async () => {
  const config = await getScraperConfig();
  return matchesAnySiteRule(config, document.URL);
};

// True if the page matches the server's rule for page type "JOB" AND
// the rule's selectors actually find matching content in the DOM.
const isJobPage = async () => {
  const config = await getScraperConfig();
  const match = matchSiteConfig(config, PAGE_TYPE.JOB, document.URL);
  return isTestOverrideActive()
    ? true
    : !!(match?.siteMatch) && !!(match?.data);
};
04EvidenceFIELD TABLE
What the remote config file controls
FieldValueWhy it matters
Per-site selector rules
indeed.com/cmp/.+/(jobs|\?.*jk=) → job.role: "[data-testid='jobDetailTitle']"CSS selectors marking which elements hold job title, company, description, location, and pay per site.
DOM change watchers
monster.com/jobs/, glassdoor.(com|sg|ca|co.uk)/Job/Tells the extension which page sections to keep watching for updates after the initial load, for sites that load job content dynamically.
Job-posting detector heuristics
knownUrls: "boards.greenhouse.io/.*/jobs/.*"; goodSubmitBtn matches "submit application"URL patterns and page-text rules used to decide if the current page is worth scraping.
Covered domains
linkedin.com, indeed.com, glassdoor.com, google.com, dice.com, builtin.com, and 55 othersThe list of job-search and career sites the extension is currently configured to read, including major boards and Google search results.
05EvidenceTHIRD PARTY LIST
Where the configuration is served from
  • s3.amazonaws.com

    Hosts Teal's config.v4.json file, an Amazon S3 object (not a Teal-branded domain) that lists the CSS-selector rules used to read job-listing pages across 61+ sites.

06EvidenceARTIFACT
Check if you're affected

Fetches the extension's live remote configuration and lists every job-site domain it currently controls scraping behavior for, so you can check the current live scope without installing the extension.

RequiresPython 3.8+
fetch_teal_config.py · py
#!/usr/bin/env python3
"""Fetch Teal's live remote scraper config and list every job-site domain
it currently controls DOM-extraction behavior for."""
import json
import urllib.request

CONFIG_URL = "https://s3.amazonaws.com/teal.extension/config.v4.json"


def main():
    with urllib.request.urlopen(CONFIG_URL) as resp:
        body = resp.read()
    config = json.loads(body)
    scraper = config.get("scraper", {})
    domains = sorted({key.split("/")[0] for key in scraper})
    print(f"Fetched {len(body)} bytes from {CONFIG_URL}")
    print(f"{len(scraper)} site-path rules covering {len(domains)} unique domains:\n")
    for domain in domains:
        print(f"  {domain}")


if __name__ == "__main__":
    main()
How to run it
  1. 1
    Run `python3 fetch_teal_config.py` (no install needed; publicly readable).
  2. 2
    Compare the domain list against what the CWS listing discloses.

What it can do

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

  • Store data in your browser

    storage

  • See the address and title of every tab you have open

    tabs

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

    activeTab

  • Show a panel beside the page

    sidePanel

Updated 30 September 2026opafjjlpbiaicbbgifbejoochmmeikep