Is Attach Files as PDF safe?

Medium risk

Attach Files as PDF reads every signed-in Google account email on Gmail load and sends the list to cloudHQ.

When Gmail loads, the extension checks each signed-in Google account by fetching Gmail account-switcher pages and reading the email addresses. It posts that list, including accounts never used with the extension, to cloudHQ at most once every two weeks, with no consent prompt beforehand.

Strong link to a flagged operator: 3 of the 12 other listings from the same operator are rated high or critical for malicious behaviour. Flagged: Save Emails as PDF by cloudHQ (high), Save Emails to Drive (high), Label Sharing for Gmail™ by cloudHQ (high).

cloudHQv1.0.0.5Chrome Web Store
49Risk
Who publishes it

cloudHQ LLC - 12 other listings from the same operator, 4 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 LLC

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

Publishers can request a review.

Findings

Gmail load sends every signed-in Google account email to cloudHQ

Code analysis shows the extension reads the email of each Google account signed in to the browser and posts the list to cloudhq.net, at most every two weeks.

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

You open Gmail while signed in to one or more Google accounts.

The extension did this

The extension collects the email of every signed-in account and sends the list to cloudhq.net.

No dialog appears first, and accounts you never used with the extension are included.

Fields in the POST to main_pre_user/check
  • Current account email
    jane.doe@gmail.com

    Identifies the account you are using in this Gmail tab.

  • Other signed-in emails

    Lists every other Google account signed in to this browser profile.

  • Extension name
    Attach Files as PDF

    Says which cloudHQ extension sent the report.

  • Login mode flag
    switch_login=1

    A fixed marker sent with every request.

When this fires

Every 14 days

The check repeats only when the stored timestamp is missing or older than two weeks, so it recurs about every 14 days per account.

The code that does this

Precheck that gathers the account list and posts it

Readable version

fn_pre_user_check

content.js
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)        }      };

fn_get_switcher_account_emails

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)        }      };
Where the account list goes
    • www.cloudhq.net

    cloudHQ backend receiving main_pre_user/check, the extension vendor's own server.

    • mail.google.com

    Queried by the extension for mail/u/N/?sw=2 to read each signed-in account 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.5. 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 Attach Files as PDF 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

    Attach Files as PDF sends data to www.cloudhq.net. A widely used service: 79 other extensions we have analysed send data here.

Updated 15 October 2026njobiengepfhfcmdanlbmnnhpmllgkhi