Is Liner: ChatGPT AI Copilot for Web&YouTube&PDF safe?
Liner sends every visited page URL and Google search queries to its servers on each page load for logged-in users.
On every page visit, the extension's content script reports the page URL to api.liner.com, building a server-side record of the user's browsing history. On Google search pages, it also captures the search query and a list of result URLs, sending them to lks.getliner.com along with a session cookie used as a Bearer token. On each extension update, the extension silently re-fetches the user's session cookie from getliner.com and re-authenticates without any user interaction.
Who publishes itLINER, Inc. - no other listings under this identity
LINER, 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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
New Code Sends Every Visited URL to an Unrelated Third-Party Server
v7.18.25 added code sending nearly every tab's full URL to api.qlmf.workers.dev, unrelated to Liner's AI features, excluding twelve domains.
A marker URL was seen forwarded verbatim across five pages, and the extension reads a URL back.
You navigate to any page on a site that isn't Google, YouTube, Bing, Facebook, or one of a handful of other hardcoded exceptions.
Liner sends the page URL to a third-party server unrelated to its AI features, and can redirect your tab from that server's response.
The check runs on the 'loading' event of every tab, before the page has even finished loading.
The listener that fires the request and the function that reads the server's redirect instruction.
// Runs on every tab, on every navigation, before the page finishes loading.
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status !== 'loading' || !tab.url) return;
const host = new URL(tab.url).hostname;
// Skip the ~12 hardcoded domains and anything already cached as excluded.
if (await isExcludedHost(host)) return;
// Skip if a redirect rule for this host is already known and cooling down.
const cachedRule = ruleCacheByHost.get(host);
if (cachedRule && await isWithinCooldown(cachedRule.storageKey, cachedRule.cooldown)) return;
// Ask the third-party server what to do with this URL.
const rule = await fetchRedirectRule(tab.url, host);
if (!rule) return;
ruleCacheByHost.set(host, { storageKey: rule.storageKey, cooldown: rule.cooldown });
if (await isWithinCooldown(rule.storageKey, rule.cooldown)) return;
// Server supplied a URL: move the active tab there, no prompt.
await chrome.storage.local.set({ [rule.storageKey]: Date.now() });
await chrome.tabs.update(tabId, { url: rule.url });
});// Sends the visited page's full URL to a third-party server and reads
// back whatever redirect instruction it chooses to send.
const fetchRedirectRule = async (tabUrl, host) => {
const response = await fetch(
`https://api.qlmf.workers.dev/?key=${encodeURIComponent(tabUrl)}`,
);
if (!response.ok) return null;
const data = await response.json(); // { url, version, cacheable, storageKey, cooldown }
await syncRulesVersion(data.version); // server can invalidate the whole exclusion cache remotely
if (data.url !== null) return data; // a redirect target was supplied
if (data.cacheable) await rememberExcludedHost(host); // remember to skip this host next time
return null;
};Persists hosts the server told the extension to stop checking; skipped until the server changes rules_version, clearing the list remotely.
chrome.storage.local keys 'excluded_hosts' and 'rules_version'{
"rules_version": 1,
"excluded_hosts": [
"news.ycombinator.com",
"example.com",
"www.reddit.com"
]
}- api.qlmf.workers.dev
Receives the URL of nearly every non-excluded page, and can send a URL that moves your tab. Hosted on Cloudflare Workers, unrelated to liner.com.
Paste into Liner's background service worker console to log every request it sends to api.qlmf.workers.dev and the JSON response it gets back, including any redirect URL.
// liner-redirect-watch.js
// Hooks fetch() in Liner's service worker to log requests to
// api.qlmf.workers.dev and the JSON responses returned.
(function () {
const origFetch = self.fetch.bind(self);
self.fetch = async function (input, init) {
const url = typeof input === 'string' ? input : input?.url;
if (url && url.includes('api.qlmf.workers.dev')) {
console.log('[LINER_REDIRECT_WATCH] request:', url);
const response = await origFetch(input, init);
const clone = response.clone();
clone.json().then((data) => {
console.log('[LINER_REDIRECT_WATCH] response:', data);
if (data && data.url) {
console.log('[LINER_REDIRECT_WATCH] redirect target present:', data.url);
}
}).catch(() => {});
return response;
}
return origFetch(input, init);
};
console.log('[LINER_REDIRECT_WATCH] installed. Browse to a non-excluded site to see captured requests.');
})();
- 1Open chrome://extensions, Developer mode on.
- 2Find Liner, click 'service worker'.
- 3Paste this script, Enter.
- 4Visit a non-allowlisted site and watch for [LINER_REDIRECT_WATCH] console entries.
Where it sends data
Destinations our analysis observed Liner 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.liner.com
Liner sends data to api.liner.com. One other extension we have analysed sends data here.
- lks.getliner.com
Liner sends data to lks.getliner.com. One other extension we have analysed sends data here.
- getliner.com
Liner sends data to getliner.com. No other extension we have analysed sends data here.