Is AliSave Pro - Download AliExpress Images safe?

Medium risk

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.…

AliSave Prov2.3.7Chrome Web Store
52Risk
Who publishes it

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.

Publisher
AliSave Pro

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
What actually happens
You did this

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 did this

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.

Captured request
GEThttps://api.alisavepro.com/heartbeat/

200 OK, JSON body {"heartbeat":1}

Headers
cross-origin-opener-policy
same-origin
When this fires

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.

Why you can't catch this in DevTools

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.

Decoded value
same-origin
The code that does this

Heartbeat fetch and storage, background.js

Readable version

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
What actually happens
You did this

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 did this

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.

Captured request
GEThttps://api.alisavepro.com/download-id/

200 OK, JSON body {"clientId":"zMxUQbfaZoaUOvPod1Ku4Cgc"}

Headers
cross-origin-opener-policy
same-origin
When this fires

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.

What's stored on your device

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'
Contents
{  "clientId": "zMxUQbfaZoaUOvPod1Ku4Cgc",  "lastDownloadTime": 1786901107451}
The code that does this

The 24-hour gate and the client-ID fetch it unlocks, background.js

Readable version

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
What actually happens
You did this

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.

The extension did this

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).

Why you can't catch this in DevTools

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.

Decoded value
https://s.click.
Why you can't catch this in DevTools

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.

Decoded value
same-origin
The code that does this

Morse codec and a second, array-indexing obfuscation helper, background.js

Readable version

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}
Reproduce it yourself

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.

Requires
  • Node.js 18+
decode-alisave-morse.js · js
// 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()));
How to run it
  1. 1Save as decode-alisave-morse.js.
  2. 2Run: node decode-alisave-morse.js.
  3. 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

Updated 30 September 2026ghcblfjepeeocokmmjjdfcigdbfgbejp