Is AliSave Pro - Download AliExpress Images safe?
AliSave Pro is medium risk. AliSave Pro contacts api.alisavepro.com and stores whatever it returns. A GET to .../heartbeat/ returned {"heartbeat":1} within seconds of install, no user action. Checkout code reads that value later, a remote switch, no update needed.…
Who publishes itAliSave Pro - no other listings under this identity
AliSave Pro - 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
A remote heartbeat check gates AliSave Pro's affiliate behavior
AliSave Pro contacts api.alisavepro.com and stores whatever it returns.
A GET to .../heartbeat/ returned {"heartbeat":1} within seconds of install, no user action.
Checkout code reads that value later, a remote switch, no update needed.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You install AliSave Pro, or your browser restarts with it already installed.
No AliExpress or Alibaba page needs to be open; this fires from the extension's background service worker alone.
The extension immediately contacts a server it controls and stores the answer for later.
The response value is written to local browser storage and re-checked on a recurring timer.
200 OK, JSON body {"heartbeat":1}
- cross-origin-opener-policy
- same-origin
Every 1 hour
The extension re-checks the heartbeat roughly once per hour while the browser is running, and unconditionally once per day via a separate startup/update handler, for as long as the extension stays installed.
The header sent on every heartbeat request isn't written as a plain string in the source, it's spelled out in Morse code and decoded at runtime, even though the header itself (a standard cross-origin-opener-policy value) has nothing sensitive to hide.
same-originHeartbeat fetch and storage, background.js
Renamed for readability
async function fetchHeartbeat(url) { try { const res = await fetch(url, { method: 'GET', headers: headersWithDecodedCOOP }); const body = JSON.parse(await res.text()); return (body.heartbeat === 1 || body.heartbeat === 11) ? body.heartbeat : 'none'; } catch { return 'none'; }}function runDailyHeartbeatCheck() { const now = new Date(); chrome.storage.local.get(['lastHeartBeatTime'], async (stored) => { const stale = !stored.lastHeartBeatTime || (now - new Date(stored.lastHeartBeatTime)) > ONE_DAY_MS; if (stale) { const value = await fetchHeartbeat(HEARTBEAT_URL); chrome.storage.local.set({ lastHeartBeatTime: now.getTime() }); if (value !== undefined) chrome.storage.local.set({ heartbeat: value }); } });}chrome.runtime.onStartup.addListener(runDailyHeartbeatCheck);chrome.runtime.onInstalled.addListener(runDailyHeartbeatCheck);AliSave Pro assigns a persistent client ID from a remote server
AliSave Pro fetches a permanent client ID from api.alisavepro.com/download-id/ and stores it locally as 'clientId', firing only 24+ hours after the last heartbeat, not on fresh install.
We confirmed a live request with a matching stored ID.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
AliSave Pro has been installed in your browser for more than 24 hours since its last check-in.
No AliExpress or Alibaba page needs to be open; this happens from the extension's background service worker alone, gated purely by an elapsed-time check in local storage.
The extension fetches a permanent identifier from a server it controls and stores it in local browser storage.
The identifier is written under the 'clientId' key and persists for as long as the extension stays installed.
200 OK, JSON body {"clientId":"zMxUQbfaZoaUOvPod1Ku4Cgc"}
- cross-origin-opener-policy
- same-origin
Every 1 day
The client-ID fetch is gated behind the same 24-hour clock the extension uses for its daily heartbeat check: it only fires once local storage shows more than a day has passed since the last check, and re-fires on the next startup or browser restart after that, for as long as the extension stays installed.
clientId is the permanent per-install ID from api.alisavepro.com; lastDownloadTime records when fetched. Both persist regardless of login.
- Location
- chrome.storage.local, keys 'clientId' and 'lastDownloadTime'
{ "clientId": "zMxUQbfaZoaUOvPod1Ku4Cgc", "lastDownloadTime": 1786901107451}The 24-hour gate and the client-ID fetch it unlocks, background.js
Renamed for readability — the client-ID fetch only runs inside the 24h-stale branch
const API_HOST = 'api.alisavepro.com';const HEARTBEAT_URL = `https://${API_HOST}/heartbeat/`;const CLIENT_ID_URL = `https://${API_HOST}/download-id/`;async function fetchClientId(url) { try { const res = await fetch(url, { method: 'GET', headers: headersWithDecodedCOOP }); const body = JSON.parse(await res.text()); return body.clientId; } catch { return null; }}async function fetchAndStoreClientId(url) { try { const clientId = await fetchClientId(url); if (clientId === null) return null; chrome.storage.local.set({ clientId, lastDownloadTime: Date.now() }); return clientId; } catch { return null; }}function runDailyHeartbeatCheck() { const now = new Date(); chrome.storage.local.get(['lastHeartBeatTime'], async (stored) => { if (stored.lastHeartBeatTime) { const staleForOverADay = (now - new Date(stored.lastHeartBeatTime)) > ONE_DAY_MS; if (staleForOverADay) { const heartbeat = await fetchHeartbeat(HEARTBEAT_URL); await fetchAndStoreClientId(CLIENT_ID_URL); // <-- only reachable from this branch chrome.storage.local.set({ lastHeartBeatTime: now.getTime() }); if (heartbeat) chrome.storage.local.set({ heartbeat }); } } else { // Fresh install: only the heartbeat runs. The client-ID fetch is never reached here. const heartbeat = await fetchHeartbeat(HEARTBEAT_URL); chrome.storage.local.set({ lastHeartBeatTime: now.getTime() }); if (heartbeat) chrome.storage.local.set({ heartbeat }); } });}chrome.runtime.onStartup.addListener(runDailyHeartbeatCheck);chrome.runtime.onInstalled.addListener(runDailyHeartbeatCheck);AliSave Pro encodes its network endpoint and header value as Morse code
AliSave Pro's background script uses Morse code to store its affiliate-redirect prefix and header value instead of plain text. decode() reproduces them exactly: https://s.click. and same-origin, adding no protection, just hiding plaintext.
- Severity
- Low unwanted
- Type
- Unexpected
- CWE
- CWE-506
- Source
- Dynamic sandbox
AliSave Pro's background script loads two configuration strings written as Morse code instead of plain text.
No installed extension setting or user action controls this; the literals are compiled into every copy of the extension.
Before either string is used, the extension runs its own Morse decoder and gets back the real values.
The decoded strings are an affiliate-redirect endpoint prefix (https://s.click.) and a request header value (same-origin).
The prefix of AliSave Pro's affiliate-redirect host is written as Morse code in the shipped source and decoded back to a URL fragment before use.
https://s.click.A second literal, the value of a cross-origin-opener-policy header sent on the extension's own outgoing requests, is Morse-encoded the same way, even though the value itself carries no sensitive information.
same-originMorse codec and a second, array-indexing obfuscation helper, background.js
Renamed for readability, logic unchanged
const MORSE = { A: '.-', B: '-...', /* ...full alphabet/digits/punctuation... */ };const MORSE_REVERSE = Object.entries(MORSE).reduce((map, [char, code]) => (map[code] = char, map), {});const morseCodec = { encode: (text) => text.toUpperCase().split('').map((c) => MORSE[c] || '').join(' '), decode: (code) => code.split(' ').map((c) => MORSE_REVERSE[c] || '').join(''),};Renamed for readability — U() works as intended, O() does not
function reconstructPathSuffix() { const chars = ['a', '/', 'b', 'p', 'c', '*', 'd', 'e']; const indices = [1, 3, 1, 5]; // all in range 0-7 return indices.map((i) => chars[i]).join(''); // -> '/p/*'}function reconstructMatchString() { const chars = ['x', 'c', 'y', 'o', 'z', 'n']; // only 6 elements, indices 0-5 const indices = [1, 3, 5, 6, 8, 10, 11]; // 6, 8, 10, 11 are out of range return indices.map((i) => chars[i]).join(''); // chars[6..11] is undefined -> // returns 'con' + 'undefined' four times over, not a clean short string}Reproduces AliSave Pro's own Morse encode/decode table and decode() function, then runs it against the two Morse literals shipped in background.js, plus the O()/U() array-indexing helpers, to show exactly what each one evaluates to.
- Node.js 18+
// Reproduces the Morse table + decode() from AliSave Pro v2.3.7's background.js// (lines 1139-1204), and the O()/U() array-indexing helpers (lines 1221-1243).const S = { A: '.-', B: '-...', C: '-.-.', D: '-..', E: '.', F: '..-.', G: '--.', H: '....', I: '..', J: '.---', K: '-.-', L: '.-..', M: '--', N: '-.', O: '---', P: '.--.', Q: '--.-', R: '.-.', S: '...', T: '-', U: '..-', V: '...-', W: '.--', X: '-..-', Y: '-.--', Z: '--..', 0: '-----', 1: '.----', 2: '..---', 3: '...--', 4: '....-', 5: '.....', 6: '-....', 7: '--...', 8: '---..', 9: '----.', '.': '.-.-.-', ',': '--..--', '?': '..--..', "'": '.----.', '!': '-.-.--', '/': '-..-.', '(': '-.--.', ')': '-.--.-', '&': '.-...', ':': '---...', ';': '-.-.-.', '=': '-...-', '+': '.-.-.', '-': '-....-', t: '..--.-', '"': '.-..-.', $: '...-..-', '@': '.--.-.', ' ': ' ',};const E = Object.entries(S).reduce((map, [char, code]) => (map[code] = char, map), {});const B = { encode: (e) => e.toUpperCase().split('').map((c) => S[c] || '').join(' '), decode: (e) => e.split(' ').map((c) => E[c] || '').join(''),};const endpointMorse = '.... - - .--. ... ---... -..-. -..-. ... .-.-.- -.-. .-.. .. -.-. -.- .-.-.-';const headerMorse = '... .- -- . -....- --- .-. .. --. .. -.';console.log('endpoint decoded:', B.decode(endpointMorse).toLowerCase());console.log('header decoded :', B.decode(headerMorse).toLowerCase());function O() { const e = ['x', 'c', 'y', 'o', 'z', 'n']; const idx = [1, 3, 5, 6, 8, 10, 11]; let o = ''; for (const n of idx) o += e[n]; return o;}function U() { const e = ['a', '/', 'b', 'p', 'c', '*', 'd', 'e']; const idx = [1, 3, 1, 5]; let o = ''; for (const n of idx) o += e[n]; return o;}console.log('O() returns :', JSON.stringify(O()));console.log('U() returns :', JSON.stringify(U()));- 1Save as decode-alisave-morse.js.
- 2Run: node decode-alisave-morse.js.
- 3Compare the printed output to the decoded values shown above.
What it can do
Permissions this extension asks for, as declared in version 2.3.7. 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 alibaba.com
*://*.alibaba.com/*
Read and change your data on aliexpress.com
*://*.aliexpress.com/*
Read and change your data on aliexpress.ru
*://*.aliexpress.ru/*
Read and change your data on aliexpress.us
*://*.aliexpress.us/*
Read and change your data on alisavepro.com
*://*.alisavepro.com/*
Store data in your browser
storage
Watch every request your browser makes
webRequest
Start, monitor and manage your downloads
downloads