Is Firelinks URL Shortner safe?

Medium risk

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.

45Risk
Who publishes it

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.

Publisher
VD

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

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

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.

Captured request
GEThttps://dsnetx.web.app/apps/firelinks/msg.json

200 JSON response observed for the message configuration request; no request body is present for this GET.

Headers
Cache-Control
no-store
Fields that decide whether a remote message applies
  • Extension identifier
    ExtFl-CH-v21

    Lets the message service send rules for this specific extension and browser build.

  • Request cache value
    r=0.7345129086412635

    Makes each check look fresh instead of reusing an older message response.

  • Private message targets
    ExtFl-CH-v21@chrome#=21

    Lets a returned message apply only when its target string matches this extension, browser, and version.

  • Display window and repeat rules
    begin=260715, expire=260731, repeatXHours=24

    Lets the returned configuration control when a message can appear and whether it can repeat.

  • Popup display type
    typeNotif=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.

The code that does this

Popup startup fetches and applies remote message rules

Readable version

The popup schedules shared.js message loading

deobfuscated/scripts/popup.js
import { 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.js
const 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;  }}
Remote message endpoints in the shipped code
    • 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

Updated 30 September 2026epdgnnildheopikdpdooanjlafgndmmk