Is Crystal safe?

Medium risk

Crystal Knows is medium risk. Visiting a LinkedIn profile with Crystal installed fetches DOM selectors from an S3 config and api.crystalknows.com/v3/selectors, both returning HTTP 200 in testing. Selectors are remote, so Crystal can change them without an update.

Crystal Knows Chrome Extensionv12.3.3Chrome Web Store
45Risk
Who publishes it

Crystal Project Inc - no other listings under this identity, 1 shared hostname

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Crystal Knows Chrome Extension
Declared legal entity
Crystal Project Inc
Registered address
9450 SW GEMINI DR, PMB 72836, Beaverton, OR 97008, US
Registered contact
Paul Jones

Shared hosts - 1 hostname

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.

assetsconfigcdn.org
Also called by 5 other listings, including Guppy - Collect smarter with AI., Bardeen

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-controlled CSS selectors determine what Crystal reads from LinkedIn

Visiting a LinkedIn profile with Crystal installed fetches DOM selectors from an S3 config and api.crystalknows.com/v3/selectors, both returning HTTP 200 in testing.

Selectors are remote, so Crystal can change them without an update.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to any page on linkedin.com with Crystal installed.

The extension did this

Crystal fetches its CSS selector configuration from Crystal-controlled servers before reading your LinkedIn page.

The extension asks two remote endpoints which DOM elements to target, meaning the server can change what the extension reads from LinkedIn pages without pushing an extension update.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://crystal-cdn.s3.amazonaws.com/linkedin_wa/config.json?v=1745027134821
HTTP 200, 533 bytes. JSON object with keys: linkedInInlineSelector, linkedInEditableDivSelector, inlineUrlContainer, linkedInPopupSelector, each an array of CSS selector strings used to identify LinkedIn messaging UI elements.
Headers
Originchrome-extension://nmaonghoefpmlfgaknnboiekjhfpmajh
03EvidenceFIELD TABLE
Selector keys returned by the remote config (from captured 200 response):
FieldValueWhy it matters
linkedInInlineSelector
.msg-convo-wrapper.msg-thread.msg-thread--pillar.relative.display-flexCSS selectors that identify the LinkedIn inline messaging thread, used to attach Crystal's email coach to messages you write.
linkedInEditableDivSelector
.msg-form__contenteditable.t-14.t-black--light.t-normal.flex-grow-1.full-height.notranslateCSS selector for the editable compose field in LinkedIn messages, where Crystal intercepts your typed text.
linkedInPopupSelector
.msg-convo-wrapper.msg-overlay-conversation-bubble.msg-overlay-conversation-bubble--default-inactive.ml4CSS selector for floating LinkedIn message bubbles, lets Crystal attach its email coach to pop-out conversations.
inlineUrlContainer
.msg-thread__link-preview-container aSelector for a URL-bearing element in LinkedIn's inline view, used to detect the current profile context.
04EvidenceCODE COMPARE
The code that does this

Extension source showing how remote selectors are fetched and applied

What it actually does
Hardcoded fallback selectors (only used if remote fetch fails)
// Built into the extension — applied only when the remote fetch throws
const FALLBACK_WA_SELECTORS = {
  linkedInInlineSelector: [".msg-convo-wrapper.msg-thread..."],
  linkedInEditableDivSelector: [".msg-form__contenteditable..."],
  linkedInPopupSelector: [".msg-convo-wrapper.msg-overlay-conversation-bubble..."]
};
this.waSelectors = FALLBACK_WA_SELECTORS; // overwritten on initialize()
fetchWASelectors() — content script fetches messaging selector config from S3
async fetchWASelectors() {
  try {
    const resp = await fetch(
      `https://crystal-cdn.s3.amazonaws.com/linkedin_wa/config.json?v=${Date.now()}`
    );
    // Returns { elements: { linkedInInlineSelector: [...], linkedInEditableDivSelector: [...], ... } }
    return (await resp.json()).elements;
  } catch {
    return false; // falls back to hardcoded JF selectors
  }
}
fetchProfileSelectors() — content script asks background SW for /v3/selectors
// Content script sends a message; background SW makes the authenticated API call
const fetchProfileSelectors = () =>
  new Promise(async resolve => {
    chrome.runtime.sendMessage({ type: 'GET_SELECTORS', appName: ..., selectorClass: ... }, result => {
      resolve(result);
    });
  });
Background SW — proxies /selectors and /manage/selectors to the authenticated API
// Handles the GET_SELECTORS message from the content script
case 'GET_SELECTORS':
  return getAuthTokens().then(tokens => {
    buildApiClient(tokens).get('/selectors')
      .then(r => r.json())
      .then(data => respond(data))
      .catch(() => respond(null));
  }), true;
scrape() — assembles profile scraper using the remote selectors
this.scrape = async callback => {
  const profileElements = await this.getProfileElements();
  if (!this.getProfileQuery(profileElements)) return false;

  let scraper;
  if (isSalesNavigatorProfile()) {
    // Fetches linkedin/config.json from S3 for Sales Navigator selectors
    this.linkedinScrapeConfig = await this.fetchLinkedinSelectors();
    scraper = new SalesNavigatorScraper(this.linkedinScrapeConfig.salesNavigator);
  } else if (this.isRecruiterProfile()) {
    scraper = new RecruiterScraper();
  } else if (isTalentProfile()) {
    scraper = new TalentScraper();
  } else {
    // Uses this.selectors — the remotely-fetched set from /v3/selectors
    scraper = new StandardScraper(this.selectors);
  }

  const scraped = await scraper.scrape();
  // Sends result to crystalknows.com API for personality prediction
  chrome.runtime.sendMessage(buildProfileQuery(scraped));
};
05EvidenceTHIRD PARTY LIST
Endpoints delivering the remote selector configuration:
  • crystal-cdn.s3.amazonaws.com

    AWS S3 bucket operated by Crystal. Serves linkedin/config.json (Sales Navigator selectors) and linkedin_wa/config.json (messaging UI selectors) as unauthenticated JSON files.

  • api.crystalknows.com

    Crystal's authenticated REST API. Serves /v3/selectors (profile-scraping set) and /manage/selectors (per-app set) to the background worker using the user's session token.

Updated 30 September 2026nmaonghoefpmlfgaknnboiekjhfpmajh