Is Firelinks URL Shortner safe?
Firelinks URL Shortner is medium risk. Firelinks requests a remote message config from dsnetx.web.app on popup load. The code appends the extension ID and a random cache value, parses global/private rules, and can show an alert or notification when a rule has show enabled.
Who publishes itVD - no other listings under this identity
VD - 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
Remote message config can show Firelinks popups
Firelinks requests a remote message config from dsnetx.web.app on popup load.
The code appends the extension ID and a random cache value, parses global/private rules, and can show an alert or notification when a rule has show enabled.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You open the Firelinks popup to shorten or review links.
The popup startup path schedules a remote message check about 1.5 seconds later.
The extension asks a remote host for message rules that can decide what popup to show.
A matching rule with display enabled can become either an alert dialog or an in-extension notification.
200 JSON response observed for the message configuration request; no request body is present for this GET.
- Cache-Control
- no-store
- Extension identifierExtFl-CH-v21
Lets the message service send rules for this specific extension and browser build.
- Request cache valuer=0.7345129086412635
Makes each check look fresh instead of reusing an older message response.
- Private message targetsExtFl-CH-v21@chrome#=21
Lets a returned message apply only when its target string matches this extension, browser, and version.
- Display window and repeat rulesbegin=260715, expire=260731, repeatXHours=24
Lets the returned configuration control when a message can appear and whether it can repeat.
- Popup display typetypeNotif=WEBALERT
Controls whether a matching message appears as a browser alert or as a notification inside the extension UI.
- Stored message state__notify__db.lastmsgid=10011002
Lets the extension remember which message IDs have already been shown and when repeatable messages last appeared.
Popup startup fetches and applies remote message rules
The popup schedules shared.js message loading
deobfuscated/scripts/popup.jsimport { c, g as h, A as b, H as f } from "./shared.js";import { f as g, l as u, g as o, h as m, i as w, a as L } from "./common.js";function A() { setTimeout(() => { c(); }, 1500);}let r;$(() => { g.load().then(() => { u(); w(L.privacyConfig.collectUsageStats); r = new v(); r.init(); A(); }).catch(s => { o.gaEventAL(`${o.label.Popup}-Load`, o.vals.Fail); m.error("Error while loading popup ", s); });});The shared bundle fetches msg.json and turns matching rules into alerts or notifications
deobfuscated/scripts/shared.jsconst u = { set(t, e) { if (!this.hasSupport()) { this.log("can't store"); return; } U(t, e); }, put(t, e) { this.set(t, e); }, setItem(t, e) { return this.set(t, e); }, append(t, e) { if (!this.hasSupport()) { this.log("can't store"); return; } const r = P(t); if (r) e = r + e; U(t, e); }, getItem(t) { return this.get(t); }, get(t) { if (!this.hasSupport()) { this.log("No storage support"); return; } return P(t) || null; }, removeItem(t) { return this.delete(t); }, delete(t) { return U(t, void 0); }, hasSupport() { return typeof Storage < "u"; }, log(t) {}};const f = V.APP_ID;const A = globalThis.CONFIGS && globalThis.CONFIGS.TEST;const T = [ "https://dsnetx.web.app/apps/firelinks/msg.json", "https://dsnet.bitbucket.io/apps/ext/msg/msg.json"];let L = false;const tt = 7;const et = 22;let d = 0;function S(t) { let e; try { if (t == null) return true; if (typeof t == "number") return false; if (t && Object.keys(t).length === 0) return true; if (typeof t == "object") { for (const r in t) { e = t[r]; if (e !== void 0 && typeof e != "function") return false; } } else if (typeof t == "string" && t.trim().length > 0) { return false; } } catch (r) { this.error(`couldn't check emptyness: ${r}`); } return true;}function w(t, e, r) { a.gaEventALV(t, e, r);}function I() { m.log("Loading messages..."); const t = `${T[d]}?ref=${f}&r=${Math.random()}`; fetch(t, { cache: "no-store" }) .then(e => e.json()) .then(e => { w(f, "Notif", "Loaded"); S(e) ? (d++, m.log(`Failed, retrying - ${d}`), d < T.length && setTimeout(I, 100)) : (d = 0, rt(e)); }) .catch(e => { console.log("error in ", e); d++; m.log(`Failed, retrying - ${d}`); d < T.length ? setTimeout(I, 100) : d = 0; });}function rt(t) { const e = []; let r, n; const s = B(); let o = false; r = n = null; try { const i = t.global.exclude; for (let l = 0; l < i.length; l++) { try { if (i[l] === s) { o = true; break; } } catch (h) { m.log(h.message); } } if (!o) { try { n = t.global; } catch (l) { m.log(l.message); } } const c = t.private; for (let l = 0; l < c.length; l++) { try { const h = c[l].ids; for (let E = 0; E < h.length; E++) { try { if (it(h[E])) { if (c[l] && c[l].hasOwnProperty("begin") && Number(c[l].begin) > Number(F()) || c[l] && c[l].hasOwnProperty("expire") && Number(c[l].expire) < Number(F())) continue; r = c[l]; e.push(r); } } catch {} } } catch (h) { m.log(h.message); } } } catch (i) { w(f, "Notif", "ProcessMessageError"); m.log(i.message); } for (let i = 0; i < e.length; i++) D(e[i]); if (n && n.show) { setTimeout(() => { D(n); L = false; }, L ? 2e3 : 10); }}function D(t) { if (t && (t.show === true || t.show === "true")) { const e = new Date().getHours(); if (e >= tt && e <= et || A) { if (t.repeat === true) { if (t.hasOwnProperty("repeatXHours")) { nt(t) && N(t); return; } N(t); return; } const r = u.get("lastmsgid"); r === null || r.indexOf(t.msgid) < 0 ? (N(t), u.append("lastmsgid", t.msgid), r && r.length > 300 && u.put("lastmsgid", t.msgid)) : m.log("** Either invalid message or already notified"); } }}function nt(t) { try { if (A) return true; const e = Number(t.msgid); const r = Number(t.repeatXHours); if (r === 0) return false; let n = u.get("repeatStore"); const s = new Date().getTime(); if (!S(n)) { n = JSON.parse(n); } else { n = {}; n[e] = { last: s }; u.put("repeatStore", JSON.stringify(n)); return true; } const o = n[e]; if (o) { const i = Number(o.last); return ot(i) < r * 60 ? false : (n[e].last = s, u.put("repeatStore", JSON.stringify(n)), true); } if (Object.keys(n).length > 50) n = {}; n[e] = { last: s }; u.put("repeatStore", JSON.stringify(n)); return true; } catch {} return status;}function B() { return A ? "DrNNAAA" : f;}function N(t, e) { const r = !S(t) && t.hasOwnProperty("typeNotif") ? t.typeNotif : "WEBUI"; if (r !== "NATIVE") { r === "WEBALERT" ? (alert(`${t.title}: ${t.msg}`), L = true) : st(t); }}function st(t, e) { if (!$.notify.getStyle("bootstrap-html")) { const r = $.notify.getStyle("bootstrap"); r.html = `<div><span data-notify-html></span></div>`; $.notify.addStyle("bootstrap-html", r); } try { if (t.autoclose) { const r = { autoHide: true, autoHideDelay: t.autoclose, className: t.type, style: "bootstrap-html" }; $.notify(t.msg, r); } else { const r = { autoHide: false, className: t.type, style: "bootstrap-html" }; $.notify(t.msg, r); $("span[data-notify-text]").css({ "font-size": "medium", "font-weight": "400" }); } w(f, "Notif-FinalMessageSent", t.msgid); } catch (r) { console.log(r); w(f, "Notif-Failed-vnotify", t.msgid); }}function at() { const t = function(e) { return e.test(window.navigator.userAgent); }; switch (true) { case t(/Edg/i): return "edge"; case t(/edge/i): return "edge"; case t(/opr/i) && (!!window.opr || !!window.opera): return "opera"; case t(/chrome/i) && !!window.chrome: return "chrome"; case t(/trident/i): return "ie"; case t(/firefox/i): return "firefox"; case t(/safari/i): return "safari"; default: return "other"; }}function F() { const t = new Date(); return `${t.getFullYear().toString().substr(2, 2)}${`0${t.getMonth() + 1}`.slice(-2)}${`0${t.getDate()}`.slice(-2)}`;}function ot(t) { let e = (new Date().getTime() - new Date(t).getTime()) / 1e3; e /= 60; return Math.abs(Math.round(e));}function it(t) { try { if (!S(t)) { const e = B(); if (!t.includes(e)) return false; if (t.includes("@")) { let n; try { n = t.split("@"); t = t.split("@")[0]; const s = n[1]; const o = at(); if (s && o && s !== "all" && s !== o) return false; } catch {} } const r = Number(chrome.runtime.getManifest().version.replaceAll(".", "")); if (t.includes("#")) { const n = t.split("#"); let s; if (n && n[1].includes("<")) { if (s = Number(n[1].replace("<", "")), r < s) return true; } else if (n && n[1].includes(">")) { if (s = Number(n[1].replace(">", "")), r > s) return true; } else if (n && n[1].includes("=") && (s = Number(n[1].replace("=", "")), s === r)) { return true; } return false; } return true; } return false; } catch { return false; }}- dsnetx.web.app
Primary host for Firelinks message configuration; observed returning JSON during dynamic analysis.
- dsnet.bitbucket.io
Fallback host used by the same fetch loop if the primary response is empty or fails.
What it can do
Permissions this extension asks for, as declared in version 2.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Act on the current tab, but only after you click the extension
activeTab
Add items to the right-click menu
contextMenus
Store an unlimited amount of data in your browser
unlimitedStorage
Store data in your browser
storage
Schedule its own background tasks
alarms
Run its own code inside the pages you visit
scripting