Is Kleo safe?
Kleo is medium risk. A FETCH_LINKEDIN_DATA message from kleo.so makes the extension read your li_at and JSESSIONID cookies for authenticated LinkedIn API calls. Path and method come from the caller. Canary cookies were confirmed relayed to Voyager.…
Who publishes itKleo - 1 other listing from the same operator, none carrying a finding
Kleo - 1 other listing from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 1k+ users between them, none of them carrying a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
kleo.so Can Trigger Full LinkedIn Cookie Harvest and API Proxying
A FETCH_LINKEDIN_DATA message from kleo.so makes the extension read your li_at and JSESSIONID cookies for authenticated LinkedIn API calls.
Path and method come from the caller.
Canary cookies were confirmed relayed to Voyager.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
The kleo.so website sends a FETCH_LINKEDIN_DATA message to the extension.
The message includes an action path, HTTP method, and optional request body, all caller-controlled.
The extension reads your LinkedIn session cookies and makes an authenticated LinkedIn API request on the caller's behalf.
chrome.cookies.getAll({domain: '.linkedin.com'}) returns li_at, JSESSIONID, and CSRF token; these are attached to a fetch() directed at the caller-supplied Voyager API path.
FETCH_LINKEDIN_DATA handler, cookie harvest and API proxy
case 'FETCH_LINKEDIN_DATA': return chrome.cookies.getAll({ domain: '.linkedin.com' }, async (cookies) => { try { // Extract CSRF token from JSESSIONID or ajax:JSESSIONID const jsessionCookie = cookies.find(c => c.name === 'JSESSIONID'); const ajaxCookie = cookies.find(c => c.name === 'ajax:JSESSIONID'); const csrfToken = jsessionCookie?.value || ajaxCookie?.value; if (!csrfToken) { sendResponse({ success: false, error: 'LinkedIn session not found.' }); return; } const csrfValue = csrfToken.replace(/"/g, ''); const liAtCookie = cookies.find(c => c.name === 'li_at'); if (!liAtCookie?.value) { sendResponse({ success: false, error: 'LinkedIn authentication required.' }); return; } // Action URL is fully caller-controlled const apiUrl = message.action.startsWith('http') ? message.action : 'https://www.linkedin.com/voyager/api/' + message.action; const headers = { 'Accept': 'application/vnd.linkedin.normalized+json+2.1', 'x-restli-protocol-version': '2.0.0', 'csrf-token': csrfValue, 'x-li-lang': langValue, 'x-li-page-instance': 'urn:li:page:d_flagship3_feed', 'origin': 'https://www.linkedin.com', 'Referer': 'https://www.linkedin.com/', }; const response = await fetch(apiUrl, { method: message.method || 'GET', headers, credentials: 'include', ...(message.body ? { body: message.body } : {}), }); // Full LinkedIn API response forwarded to sender sendResponse({ success: true, data: await response.json() }); } catch (err) { sendResponse({ success: false, error: err.message }); } });- LinkedIn session token (li_at)AQEDATPx...XwCx_kA
Authenticates you to LinkedIn. Any party that holds this value can make fully authenticated LinkedIn requests as you.
- Session ID / CSRF token (JSESSIONID)ajax:0123456789012345678
Used as the CSRF token header in every LinkedIn API call. Required alongside li_at to mutate LinkedIn data.
- Ajax JSESSIONID (ajax:JSESSIONID)ajax:9876543210987654321
Alternate form of the CSRF token accepted by some LinkedIn Voyager endpoints.
- Caller-supplied API path/identity/profiles/ACoAAABxyzABCD/topCard?decorationId=com.linkedin.voyager.dash.deco.identity.profile.TopCard
The API path or URL the site tells the extension to call, appended to https://www.linkedin.com/voyager/api/ unless it starts with http.
- www.linkedin.com
LinkedIn Voyager API, destination of the authenticated fetch(), called with credentials:include and the user's real session cookies.
- kleo.so
Kleo web app, listed in externally_connectable, sends the FETCH_LINKEDIN_DATA command and receives the full LinkedIn API response.
- linkedin-prod-api.kleo.so
Kleo backend, receives LinkedIn posts and profile data via a separate UPDATE_POSTS flow through the same message bus.
The manifest restricts external message senders to `*://*.kleo.so/*` and `*://*.localhost/*`, which prevents arbitrary websites from triggering this handler. However, the handler does not validate `sender.url` or `sender.origin` inside the FETCH_LINKEDIN_DATA branch. Any page served from any kleo.so subdomain — including one that is compromised, XSS-vulnerable, or served from a dangling subdomain — can instruct the extension to read the user's LinkedIn cookies and relay authenticated requests to an arbitrary LinkedIn Voyager API path. The action URL is fully caller-controlled with no allowlist.
Supabase client uses bundled anonymous API key
Kleo initializes a Supabase client for uk.server2.data.kleo.so with a bundled anonymous API key, configured to refresh and persist sessions.
The storage adapter keeps kleo_auth session state in Chrome extension local storage.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You install or start Kleo, which loads its background service worker.
The extension creates a Supabase backend client, refreshes sessions, and persists auth state in extension storage.
Dynamic analysis observed Supabase authentication traffic to the configured backend; no request body was recorded.
- Supabase project hostuk.server2.data.kleo.so
This is the remote backend that receives the extension authentication traffic.
- Bundled anonymous API keyeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzM0MzkzNjAwLAogICJleHAiOiAxODkyMTYwMDAwCn0.92Tf-L2njOh2gPu1J8RcmSwjn8yqvQv6NF1vmVHzX7I
The key identifies the Supabase project role used by the extension when it talks to the backend.
- Extension auth storage keykleo_auth
Session state stored under this key can keep the extension signed in across browser restarts.
Supabase client and storage configuration
Readable Supabase client setup
deobfuscated/static/background/index.js "5sSbd": [function(e, t, r) { var s = e("@parcel/transformer-js/src/esmodule-helpers.js"); s.defineInteropFlag(r), s.export(r, "supabaseClient", () => o); var i = e("@supabase/supabase-js"), n = e("~lib/kleoStorage"); let o = (0, i.createClient)("https://uk.server2.data.kleo.so", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzM0MzkzNjAwLAogICJleHAiOiAxODkyMTYwMDAwCn0.92Tf-L2njOh2gPu1J8RcmSwjn8yqvQv6NF1vmVHzX7I", { auth: { storage: n.kleoStorage, autoRefreshToken: !0, persistSession: !0, detectSessionInUrl: !0, storageKey: "kleo_auth" } })Readable storage adapter setup
deobfuscated/static/background/index.js j67dh: [function(e, t, r) { var s = e("@parcel/transformer-js/src/esmodule-helpers.js"); s.defineInteropFlag(r), s.export(r, "kleoStorage", () => n); var i = e("@plasmohq/storage"); let n = new i.Storage({ area: "local", copiedKeyList: ["kleo_auth"] })- uk.server2.data.kleo.so
Supabase authentication and session-management backend configured in the extension source.
What it can do
Permissions this extension asks for, as declared in version 0.265.0. 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 linkedin.com
*://*.linkedin.com/*
Read and change your data on kleo.so
*://*.kleo.so/*
Store data in your browser
storage
Run its own code inside the pages you visit
scripting
Read and change cookies, including the ones that keep you signed in
cookies