Is Attach Files as PDF safe?
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).
Who publishes itcloudHQ LLC - 12 other listings from the same operator, 4 of them carrying a finding
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.
Same store account
12 other listings published from this account, 408k+ users between them. 4 of them carry a finding.
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
You open Gmail while signed in to one or more Google accounts.
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.
- Current account emailjane.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 nameAttach Files as PDF
Says which cloudHQ extension sent the report.
- Login mode flagswitch_login=1
A fixed marker sent with every request.
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.
Precheck that gathers the account list and posts it
fn_pre_user_check
content.jsfn_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.jsfn_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) } };- 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.
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.