Is Lumaly - Gutscheine & Cashback safe?

High risk

Lumaly is high risk. The background worker fetches a JSON automation script from files.lumaly.de and hands it to the content script, which runs it as page-load, voucher, and checkout handlers, letting Lumaly control the tab. Confirmed on tchibo.de twice.…

Lumalyv3.0.2.7Chrome Web Store
75Risk
Who publishes it

Lumaly GmbH - no other listings under this identity, 4 shared hostnames

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

Publisher
Lumaly
Declared legal entity
Lumaly GmbH
Registered address
Lietzenburger Str. 77, Berlin 10719, DE
Registered contact
Noah Fischer

Shared hosts - 4 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

api.lumaly.de
Also called by 1 other listing: Lumaly
files.lumaly.de
Also called by 1 other listing: Lumaly
lu-static.fra1.cdn.digitaloceanspaces.com
Also called by 1 other listing: Lumaly
lumaly.de
Also called by 1 other listing: Lumaly

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

Publishers can request a review.

Findings

Remote-Hosted Scripts Automate Checkout Pages on Every Supported Shop

The background worker fetches a JSON automation script from files.lumaly.de and hands it to the content script, which runs it as page-load, voucher, and checkout handlers, letting Lumaly control the tab.

Confirmed on tchibo.de twice.

Severity
High unwanted
Type
Unexpected
CWE
CWE-829
Source
Dynamic sandbox
What actually happens
You did this

You navigate to a shopping site the extension supports, such as tchibo.de.

The extension did this

The background service worker fetches a JSON automation script for that shop from files.lumaly.de and runs it against the page through the content script.

The script can read page elements like the cart total and drive coupon-application and checkout actions.

Captured request
GEThttps://files.lumaly.de/ext/js/tchibo.de.json

200 OK, 11,414-byte JSON automation config, including the CSS selector [data-locator='cart_total_amount'] .tp-price-number used to read the shop's cart total.

The code that does this

Fetching the remote script and executing it against the page

Readable version

background.js: fetches {shop}.json from files.lumaly.de

background.js
ShopScriptApi.prototype.fetch = function(t, e, n, r) {              return shopScript_awaiter(this, void 0, void 0, (function() {                var o, i, a, u;                return shopScript_generator(this, (function(c) {                  switch (c.label) {                    case 0:                      return n || (n = function(t, e) {                        switch (e) {                          case d.JSON:                            return t + ".json";                          case d.JS:                            return t.split(".")[0] + ".js";                          default:                            return ""                        }                      }(t, e)), o = "".concat(p.Ct).concat(n, "?").concat(Date.now().toString().slice(0, -3)), [4, fetch(o, {                        method: "GET",                        headers: {                          "Content-Type": e === d.JSON ? "application/json" : "text/javascript"                        }                      })];                    case 1:                      if (!(i = c.sent()).ok) throw new Error(i.status.toString());                      return e !== d.JSON ? [3, 2] : [2, i.json()];                    case 2:                      return [4, this.fetchExtensionScripts(r)];                    case 3:                      return a = c.sent(), [4, i.text()];                    case 4:                      return u = c.sent(), [2, {                        requiredScript: a,                        shopScript: u                      }]                  }                }))              }))            }

content.js: compiles and runs the fetched script against the page

content.js
HandlePageLoad.prototype.handleJsonShop = function(w, z) {            return HandlePageLoad_awaiter(this, void 0, void 0, (function() {              var P, V, $, Z, X;              return HandlePageLoad_generator(this, (function(ie) {                switch (ie.label) {                  case 0:                    return ie.trys.push([0, 1, , 3]), this.autoapplyScript = function(w, z, P) {                      var V = this;                      void 0 === P && (P = {});                      var $ = (Array.isArray(w.template) ? w.template : "string" == typeof w.template ? [w.template] : []).reduce((function(w, z) {                        return function(w, z) {                          var P = pr[w];                          if (!P) throw new Error("Template with name '".concat(w, "' doesn't exist"));                          return lr(P, z, {                            arrayMerge: overwriteMerge                          })                        }(z, w)                      }), w);                      Object.freeze(P);                      var Z = parse($),                        X = Z.namedScopes,                        ee = Z.onPageLoad,                        ie = Z.onVoucher,                        ae = Z.onBestVoucher,                        ce = Z.declarations,                        le = Z.hasOnBestVoucher,                        pe = Z.isThankYouPage,                        fe = Z.onDrawerLoad,                        de = src_assign(src_assign(src_assign({}, cr), z), X);                      return {                        declarations: $.declarations,                        onPageLoad: function() {                          return src_awaiter(V, void 0, void 0, (function() {                            return src_generator(this, (function(w) {                              switch (w.label) {                                case 0:                                  return ee ? [4, executeScope(new or(ee, ce, P), de)] : [3, 2];                                case 1:                                  w.sent(), w.label = 2;                                case 2:                                  return [2]                              }                            }))                          }))                        },                        get hasOnBestVoucher() {                          return le                        },                        onVoucher: function(w) {                          return src_awaiter(V, void 0, void 0, (function() {                            return src_generator(this, (function(z) {                              switch (z.label) {                                case 0:                                  return ce.$voucher = w, [4, executeScope(new or(ie, ce, P), de)];                                case 1:                                  return z.sent(), [2]                              }                            }))                          }))                        },                        onBestVoucher: function(w) {                          return src_awaiter(V, void 0, void 0, (function() {                            return src_generator(this, (function(z) {                              switch (z.label) {                                case 0:                                  return ce.$voucher = w, [4, executeScope(new or(ae, ce, P), de)];                                case 1:                                  return z.sent(), [2]                              }                            }))                          }))                        },                        isThankYouPage: function() {                          return src_awaiter(V, void 0, void 0, (function() {                            var w;                            return src_generator(this, (function(z) {                              switch (z.label) {                                case 0:                                  return [4, executeScope(w = new or(pe, ce, P), de)];                                case 1:                                  return z.sent(), [2, !0 === w.lastEvaluation()]                              }                            }))                          }))                        },                        onDrawerLoad: function() {                          return src_awaiter(V, void 0, void 0, (function() {                            return src_generator(this, (function(w) {                              switch (w.label) {                                case 0:                                  return fe ? [4, executeScope(new or(fe, ce, P), de)] : [3, 2];                                case 1:                                  w.sent(), w.label = 2;                                case 2:                                  return [2]                              }                            }))                          }))                        }                      }                    }(w, this.createAdapter(), {                      no_logs: !Kn.eD                    }), this.checkIsThankYouPage(), P = !this.autoapplyScript.declarations.$discountAmount, V = {}, $ = null, /closing/i.test(this.autoapplyScript.declarations.$whenToApply) ? $ = "closing" : this.autoapplyScript.declarations.$whenToApply && (0, ee.Cv)("%cUnclear $whenToApply", "color: tomato; font-weight: bold;", this.autoapplyScript.declarations.$whenToApply), P && (V.onUpdateCartOldTotal = this.updateCartOldTotal.bind(this)), this.shopRunner.initializeHandlers(HandlePageLoad_assign(HandlePageLoad_assign(HandlePageLoad_assign({}, z), {                      findAppliedVoucherInDOM: this.jsonFindAppliedVoucher.bind(this),                      shouldShopBeReloaded: this.shouldShopBeReloaded.bind(this),                      onVoucher: this.autoapplyScript.onVoucher,                      onBestVoucher: this.autoapplyScript.onBestVoucher,                      hasOnBestVoucher: this.autoapplyScript.hasOnBestVoucher,                      whenToApply: $                    }), V)), [3, 3];                  case 1:                    return Z = ie.sent(), [4, this.shopRunner.errorHandler({                      code: "0-4-0-404",                      extraMsg: null == Z ? void 0 : Z.message                    })];                  case 2:                    return ie.sent(), [3, 3];                  case 3:                    return ie.trys.push([3, 10, , 12]), this.autoapplyScript ? this.autoapplyScript.declarations.$drawerConfig ? ((0, ee.Cv)("handle call handleOnPageLoadWithDrawer"), [4, this.handleOnPageLoadWithDrawer()]) : [3, 5] : [3, 9];                  case 4:                    return ie.sent(), this.observeDrawer(), [3, 7];                  case 5:                    return (0, ee.Cv)("handle call onPageLoad"), [4, this.autoapplyScript.onPageLoad()];                  case 6:                    ie.sent(), ie.label = 7;                  case 7:                    return [4, this.observeTotal()];                  case 8:                    ie.sent(), this.locationChangeObserver(), (0, ee.Cv)("Autoapply-v3 page loaded hook done"), ie.label = 9;                  case 9:                    return [3, 12];                  case 10:                    return X = ie.sent(), [4, this.shopRunner.errorHandler({                      code: "0-3-1-000",                      extraMsg: null == X ? void 0 : X.message                    })];                  case 11:                    return ie.sent(), (0, ee.Cv)("Error Autoapply-v3 loading crashed: ", X), [3, 12];                  case 12:                    return [2]                }              }))            }))          }
Reproduce it yourself

Fetches the same per-shop automation config the extension requests from files.lumaly.de, so you can see exactly what selectors and checkout logic Lumaly is serving for a given domain without installing the extension.

Requires
  • Node.js 18+ (built-in fetch)
lumaly-fetch-shop-script.js · js
#!/usr/bin/env node// lumaly-fetch-shop-script.js//// Fetches the same automation config Lumaly's background service worker// requests for a given shop domain, so you can inspect it yourself without// installing the extension. Run against your own shop domain of interest.//// Usage: node lumaly-fetch-shop-script.js tchibo.deconst domain = process.argv[2];if (!domain) {  console.error("Usage: node lumaly-fetch-shop-script.js <shop-domain>");  console.error("Example: node lumaly-fetch-shop-script.js tchibo.de");  process.exit(1);}const url = `https://files.lumaly.de/ext/js/${domain}.json?${Date.now().toString().slice(0, -3)}`;fetch(url, {  method: "GET",  headers: { "Content-Type": "application/json" },})  .then(async (res) => {    if (!res.ok) {      console.error(`No automation script published for "${domain}" (HTTP ${res.status}).`);      process.exit(1);    }    const body = await res.text();    console.log(`GET ${url}`);    console.log(`Status: ${res.status} ${res.statusText}`);    console.log(`Bytes: ${body.length}`);    console.log("---");    try {      console.log(JSON.stringify(JSON.parse(body), null, 2));    } catch {      console.log(body);    }  })  .catch((err) => {    console.error("Request failed:", err.message);    process.exit(1);  });
How to run it
  1. 1node lumaly-fetch-shop-script.js tchibo.de
Server that supplies the executed automation logic
    • files.lumaly.de

    Serves the JSON automation script the extension parses and executes on supported shop pages, including the CSS selectors and checkout-flow logic it runs against the page.

Lumaly reuses one install ID across shopping requests

Lumaly stores a lead_id at install and reuses it in requests to api.lumaly.de, including coupon, shop-list, stats, transaction, and account calls, tying shop browsing and purchases to your browser installation.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You install Lumaly or browse a supported shopping site.

The extension did this

The extension stores one lead_id and attaches it to later shopping-related requests.

Dynamic analysis observed the same stored identifier in later actions, coupon, and shop requests.

Captured request
POSThttps://api.lumaly.de/api/lead/install

The install endpoint returns the lead_id that the extension stores; later observed api.lumaly.de requests reused the same stored value.

Identifier and context fields tied together by the code path
  • Installation identifier
    lead_id value starting with 0ecb525f (observed prefix)

    Lets the service connect later shopping activity to the same browser installation.

  • Browser name
    chrome

    Adds device context to the installation record.

  • Extension version
    3.0.2.2

    Shows which Lumaly build made the request.

  • Shop hostname
    amazon.de (illustrative supported-shop domain)

    Connects your supported-shop page context to the same installation identifier.

  • Shopping action
    api/actions/visits (code path)

    Links coupon, visit, purchase, or shop-list activity to the same stored identifier.

The code that does this

The same stored lead_id is created once and reused in later API calls

Readable version

Install request obtains the lead ID

background.js
LeadApi.prototype.fetchLeadId = function(t) {              return lead_awaiter(this, void 0, void 0, (function() {                var e, n, r;                return lead_generator(this, (function(o) {                  switch (o.label) {                    case 0:                      return e = {                        "Content-Type": "application/json"                      }, n = this.endpoints.API_URL + (t ? this.endpoints.LEAD_ID_INSTALL_ENDPOINT : this.endpoints.LEAD_ID_ENDPOINT), r = {                        browser: getBrowserName(),                        distinct_id: tr.getDistinctId(),                        extension_version: this.defaultParams.extension_version,                        alreadyInstalled: t ? 0 : 1                      }, [4, fetch(n, {                        method: "POST",                        headers: e,                        body: JSON.stringify(r)                      })];                    case 1:                      return [4, o.sent().json()];                    case 2:                      return [2, o.sent().data.id]                  }                }))              }))            }

Coupon requests add lead_id to the query string

background.js
CouponsApi.prototype.fetch = function(t, e, n) {              var r, o, a;              return coupons_awaiter(this, void 0, void 0, (function() {                var u, c, s, l, p, f, h, y, v, b, w, _, g, m, S, A, P, O, T, x, k, E = this;                return coupons_generator(this, (function(C) {                  switch (C.label) {                    case 0:                      return [4, q.fetch()];                    case 1:                      return u = C.sent(), c = t ? urlToDomain(t) : null, s = Boolean(u && c && c in u), p = [__assign({}, this.defaultParams)], T = {}, [4, this.storage.leadId.get()];                    case 2:                      return l = __assign.apply(void 0, p.concat([(T.lead_id = C.sent() || "", T)])), c && s && (l.hostname = c), f = null !== (r = l.hostname) && void 0 !== r ? r : this.defaultShopKey, h = "".concat(this.apiUrl, "?") + new URLSearchParams(l).toString(), [4, this.storage.shopData(f).get()];                    case 3:                      return y = C.sent() || {}, !e && y && y.data && Date.now() < y.data.expired ? [2, A = {                        shop: null === (o = y.data) || void 0 === o ? void 0 : o.shop,                        random: y.data.random,                        coupons: null === (a = y.data) || void 0 === a ? void 0 : a.coupons                      }] : this.isFetching[f] ? ((0, i.Ki)("CouponsApi.fetch is busy and will retry", i.Bc.API), [2, new Promise((function(r) {                        return setTimeout((function() {                          return r(E.fetch(t, e, n))                        }), 1e3)                      }))]) : (this.isFetching[f] = !0, x = {}, k = {}, null == n ? [3, 4] : (b = n, [3, 6]));                    case 4:                      return [4, this.storage.token.get()];                    case 5:                      b = C.sent() || "", C.label = 6;                    case 6:                      x.headers = (k.token = b, k), v = x, C.label = 7;                    case 7:                      return C.trys.push([7, 12, , 13]), [4, fetch(h, v)];                    case 8:                      return (w = C.sent()).ok || (0, i.Cv)("Fetching data from '".concat(this.apiUrl, "' failed\n                    with code ").concat(w.status, " and message:"), w.text), [4, w.json()];                    case 9:                      return _ = C.sent(), g = _.error, m = _.data, g ? (delete this.isFetching[f], [2, {                        error: g,                        shop: null,                        random: !0,                        coupons: []                      }]) : (S = m.shop && function(t, e) {                        var n = detectScriptType(t.script_name, t.js) !== d.JSON || t.supported_partial;                        return {                          url: t.shop_url,                          id: t.shop_id,                          logo: t.shop_logo,                          absoluteLogo: t.shop_logo_absolute_url,                          name: t.shop_name,                          scriptName: t.script_name || "",                          isPreSupported: n,                          isHomePagePopup: t.home_page_popup,                          isSameTabLink: t.same_tab_link,                          existInSupported: e,                          affiliateUrl: t.affiliate_url,                          affiliateNetwork: t.affiliate_network,                          isCashback: t.is_cashback,                          matches: t.matches,                          productPages: Array.isArray(t.product_pages) ? t.product_pages : [],                          productSelectors: convertApiProductSelectors(t),                          productRegex: convertApiProductRegex(t),                          includePageSelectors: t && Array.isArray(t.include_page_selectors) ? t.include_page_selectors : [],                          excludePageSelectors: t && Array.isArray(t.exclude_page_selectors) ? t.exclude_page_selectors : [],                          skipFallbackParser: Boolean(t.skip_fallback_parser),                          oofSelectors: Array.isArray(t.oos_selectors) ? t.oos_selectors : [],                          oofStrings: Array.isArray(t.oos_strings) ? t.oos_strings : [],                          js: t.js || [""],                          cashback: convertApiCashbackToModel(t.cashback),                          rct_object: t.rct_object,                          wishlistSupported: t.wishlist_supported                        }                      }(m.shop, s), A = {                        shop: S,                        random: m.random,                        coupons: m.coupons.data.map(convertApiCouponToModel)                      }, [4, this.storage.shopData(f).get()]);                    case 10:                      return (P = C.sent() || {}).data = __assign(__assign({}, A), {                        expired: Date.now() + this.couponsCacheTime                      }), [4, this.storage.shopData(f).set(P)];                    case 11:                      return C.sent(), delete this.isFetching[f], [2, A];                    case 12:                      return O = C.sent(), delete this.isFetching[f], (0, i.Ki)("CouponsApi.fetch failed", i.Bc.API, {                        error: O                      }), [2, {                        error: O,                        shop: null,                        random: !0,                        coupons: []                      }];                    case 13:                      return [2]                  }                }))              }))            }

Shop-list requests add lead_id to the query string

background.js
ShopsApi.prototype.fetch = function(t) {              var e;              return void 0 === t && (t = 1), shops_awaiter(this, void 0, void 0, (function() {                var n, r, o, i, a, u, c, s, l, p, f, d;                return shops_generator(this, (function(h) {                  switch (h.label) {                    case 0:                      return t && 1 !== t ? [3, 2] : [4, this.storage.shops.getIfNotExpired()];                    case 1:                      if ((c = h.sent()) && 1 === (null === (e = c.pagination) || void 0 === e ? void 0 : e.currentPage)) return [2, c];                      h.label = 2;                    case 2:                      return this.isFetching[t] ? [2, {                        shops: [],                        pagination: null,                        error: null                      }] : (this.isFetching[t] = !0, d = {                        page: "".concat(t),                        environment: this.defaultParams.environment                      }, [4, this.storage.leadId.get()]);                    case 3:                      d.lead_id = h.sent() || "", n = d, r = "".concat(this.endpoints.API_URL + this.endpoints.SHOPS_GET_ENDPOINT, "?") + new URLSearchParams(n).toString(), h.label = 4;                    case 4:                      return h.trys.push([4, 9, , 10]), [4, fetch(r)];                    case 5:                      return [4, h.sent().json()];                    case 6:                      return o = h.sent(), i = o.error, a = o.data, delete this.isFetching[t], i ? [2, {                        shops: [],                        pagination: null,                        error: i                      }] : (u = a.shops, c = u.data, s = u.current_page, l = u.last_page, p = {                        shops: convertApiShopsToModel(c),                        pagination: convertApiPaginationToModel({                          current_page: s,                          last_page: l                        })                      }, 1 !== s ? [3, 8] : [4, this.storage.shops.setWithExpire(p, this.cacheTime)]);                    case 7:                      h.sent(), h.label = 8;                    case 8:                      return [2, shops_assign(shops_assign({}, p), {                        error: null                      })];                    case 9:                      return f = h.sent(), delete this.isFetching[t], [2, {                        shops: [],                        pagination: null,                        error: f                      }];                    case 10:                      return [2]                  }                }))              }))            }

Stats posts add lead_id to the JSON body

background.js
StatsApi.prototype.sendAction = function(t) {              var e = t.endpoint,                n = __rest(t, ["endpoint"]);              return stats_awaiter(this, void 0, void 0, (function() {                var t, r, o, a;                return stats_generator(this, (function(u) {                  switch (u.label) {                    case 0:                      if ("dev" === this.defaultParams.environment && e !== this.endpoints.PURCHASE_TOTAL) return (0, i.Cv)("Enable environment prod. Stat ".concat(e, " was not increased.")), [2, !1];                      u.label = 1;                    case 1:                      return u.trys.push([1, 4, , 5]), (0, i.Cv)("%c" + "".concat(this.endpoints.BASE + e), "color: orangered; font-weight: bold", n), a = {}, [4, this.storage.leadId.get()];                    case 2:                      return a.lead_id = u.sent() || "", a.extension_version = this.defaultParams.extension_version, t = {                        "Content-Type": "application/json"                      }, r = stats_assign(stats_assign({}, a), n), o = this.endpoints.API_URL + this.endpoints.BASE + e, [4, fetch(o, {                        method: "POST",                        headers: t,                        body: JSON.stringify(r)                      })];                    case 3:                      return u.sent(), [2, !0];                    case 4:                      return u.sent(), [2, !1];                    case 5:                      return [2]                  }                }))              }))            }
Destination that receives the identifier-linked requests
    • api.lumaly.de

    Receives the install-ID request and later coupon, shop, stats, transaction, and account API requests that include lead_id.

Updated 30 September 2026ebgofhigpedaepplnmglnedbfjemmpnh