Is Forwarded Email Importer by cloudHQ safe?

Medium risk

Forwarded Email Importer by cloudHQ reads every signed-in Google account email on Gmail and sends the list to cloudhq.net.

When Gmail loads, the extension checks Gmail account slots 0 to 20 to find all Google accounts signed in to the browser. It then sends that list of email addresses to cloudHQ's servers, at most once every two weeks, with no user action required. This is unrelated to its stated job of importing forwarded emails.

cloudHQv1.0.0.6Chrome Web Store
49Risk
Who publishes it

cloudHQ - 10 other listings from the same operator, 7 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
cloudHQ
Declared legal entity
cloudHQ

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Gmail EML importer sends all your signed-in Google emails to cloudhq.net

Code analysis shows that on Gmail load the extension probes up to 21 signed-in account slots, collects each email, and POSTs the list to www.cloudhq.net.

The importer needs none of those accounts.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Static analysis
What actually happens
You did this

You open Gmail with several Google accounts signed in to the browser.

The extension did this

The extension looks up the email of each signed-in account and sends the full list to cloudhq.net.

The lookup is skipped only if a check ran in the last two weeks.

Body of the POST to www.cloudhq.net/main_pre_user/check
  • Active account email
    alex.morgan@gmail.com

    The Gmail address of the tab you opened.

  • All signed-in emails
    alex.morgan@gmail.com, a.morgan@acme-corp.com, alex.m.home@gmail.com

    Every Google account signed in to this browser, including personal and work ones.

  • Extension name
    gmail_save_eml

    Which cloudHQ product sent the check.

  • Switch login flag
    1

    Marks the request as covering multiple accounts.

The code that does this

Account enumeration and the POST that follows

Readable version

Deobfuscated enumeration and pre-check

content.js
fn_get_switcher_account_emails = function(e, t) {        var n = this;        if (n.g_switcher_account_emails_index > n.g_switcher_account_emails_max_tries) return n.g_email_or_login && -1 == n.g_switcher_account_emails.indexOf(n.g_email_or_login.toLowerCase()) && n.g_switcher_account_emails.push(n.g_email_or_login.toLowerCase()), void e();        try {          n.g_switcher_account_emails_index++;          var i = "https://mail.google.com/mail/u/" + n.g_switcher_account_emails_index + "/?sw=2";          t ? (i = t + n.g_switcher_account_emails_index + "/?tgif=d", r().ajax({            url: i,            context: document.body,            cache: !1,            success: function(r) {              var i = /_docs_flag_initialData=([\s\S]*?)\;/gm;              try {                if ((a = i.exec(r)) && a.length > 1) {                  var o = JSON.parse(r.split("_docs_flag_initialData=")[1].trim().split("; _docs_flag_cek=")[0].trim()),                    s = null;                  o["docs-hue"] && n.fn_validate_email(o["docs-hue"]) && (s = o["docs-hue"].toLowerCase())                }              } catch (e) {                console.log("fn_get_switcher_account_emails:ex:1:", e)              }              if (!s) {                i = /__initData = ([\s\S]*?)\;/gm;                try {                  var a;                  (a = i.exec(r)) && a.length > 1 && (o = JSON.parse(r.split("__initData = ")[1].trim().split(";")[0]))[0][27][8] && n.fn_validate_email(o[0][27][8]) && (s = o[0][27][8].toLowerCase())                } catch (e) {                  console.log("fn_get_switcher_account_emails:ex:2:", e)                }              }              try {                return n.g_switcher_account_emails && n.g_switcher_account_emails[0] && n.g_switcher_account_emails[0] == s ? (n.g_email_or_login && -1 == n.g_switcher_account_emails.indexOf(n.g_email_or_login.toLowerCase()) && n.g_switcher_account_emails.push(n.g_email_or_login.toLowerCase()), void e()) : (-1 == n.g_switcher_account_emails.indexOf(s) && n.g_switcher_account_emails.push(s), void n.fn_get_switcher_account_emails(e, t))              } catch (e) {                console.log("fn_get_switcher_account_emails:3: failed e=", e)              }            },            error: function(r) {              console.log("error in callback or not logged in to #" + n.g_switcher_account_emails_index + " account!"), n.fn_get_switcher_account_emails(e, t)            }          })) : fetch(i, {            method: "GET",            mode: "cors"          }).then((e => e.text())).then((t => {            var r = /var GLOBALS=([\s\S]*?)\;/gm,              i = null;            try {              for (; i = r.exec(t);) {                var o = JSON.parse(JSON.stringify(i[1])).split(",");                if (o.length)                  for (var s = 0; s < o.length; s++)                    if (o[s] && n.fn_validate_email(o[s].replaceAll('"', ""))) {                      var a = o[s].replaceAll('"', "").toLowerCase();                      return n.g_switcher_account_emails && n.g_switcher_account_emails[0] && n.g_switcher_account_emails[0] == a ? (n.g_email_or_login && -1 == n.g_switcher_account_emails.indexOf(n.g_email_or_login.toLowerCase()) && n.g_switcher_account_emails.push(n.g_email_or_login.toLowerCase()), void e()) : (-1 == n.g_switcher_account_emails.indexOf(a) && n.g_switcher_account_emails.push(a), void n.fn_get_switcher_account_emails(e))                    }              }            } catch (e) {              console.log("fn_get_switcher_account_emails: failed e=", e)            }          })).catch((t => {            console.log("error in callback or not logged in to #" + n.g_switcher_account_emails_index + " account!"), n.fn_get_switcher_account_emails(e)          }))        } catch (e) {          console.log("fn_get_switcher_account_emails:error:", e)        }      };      fn_pre_user_check = function(e) {        var t = this;        try {          var n = t.g_email_or_login + t.g_cloudHQ_feature_name + "_pre_check_obj",            r = ((new Date).getTime(), {              value: "true",              timestamp: (new Date).getTime()            }),            i = {};          i[n] = r, chrome.storage.local.get(n, (function(r) {            if (!r || !r[n] || !r[n].timestamp || r[n].timestamp < t.g_two_weeks_ago_timestamp) try {              t.fn_get_switcher_account_emails((function() {                t.fn_ajax({                  type: "POST",                  url: t.g_server_url + "main_pre_user/check",                  dataType: "json",                  data: {                    email: t.g_email_or_login,                    email_switcher_list: t.g_switcher_account_emails,                    email_or_login: t.g_email_or_login,                    chrome_extension_name: t.g_cloudHQ_feature_name,                    switch_login: "1"                  },                  complete: function(e, t) {                    if (-1 === ["error", "timeout", "abort"].indexOf(t)) try {                      chrome.storage.local.set(i, (function() {}))                    } catch (e) {}                  }                })              }), e)            } catch (e) {              console.log("e:", e)            }          }))        } catch (e) {          console.log("precheck failed e=", e)        }      };
Where the account list goes
    • www.cloudhq.net

    Vendor server; the only host the background PROXY_AJAX relay accepts, and the destination of the list.

    • mail.google.com

    Queried as /mail/u/N/?sw=2 to read each signed-in account's email.

Observation

Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.

What it can do

Permissions this extension asks for, as declared in version 1.0.0.6. 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 mail.google.com

    https://mail.google.com/

  • Read and change your data on mail-attachment.googleusercontent.com

    https://mail-attachment.googleusercontent.com/

  • Read and change your data on inbox.google.com

    https://inbox.google.com/

  • Read and change your data on www.cloudhq.net

    https://www.cloudhq.net/

  • Run its own code inside the pages you visit

    scripting

  • Store data in your browser

    storage

  • Keep running in the background while your browser is open

    background

Where it sends data

Destinations our analysis observed Forwarded Email Importer 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.

  • www.cloudhq.netwidely used

    Forwarded Email Importer sends data to www.cloudhq.net. A widely used service: 79 other extensions we have analysed send data here.

Updated 15 October 2026eommcahppodmcdafolpbogknnlkogiag