Is Lumaly - Gutscheine & Cashback safe?
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.…
Who publishes itLumaly GmbH - no other listings under this identity, 4 shared hostnames
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.
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.
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
You navigate to a shopping site the extension supports, such as tchibo.de.
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.
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.
Fetching the remote script and executing it against the page
background.js: fetches {shop}.json from files.lumaly.de
background.jsShopScriptApi.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.jsHandlePageLoad.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] } })) })) }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.
- Node.js 18+ (built-in fetch)
#!/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); });- 1node lumaly-fetch-shop-script.js tchibo.de
- 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
You install Lumaly or browse a supported shopping site.
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.
The install endpoint returns the lead_id that the extension stores; later observed api.lumaly.de requests reused the same stored value.
- Installation identifierlead_id value starting with 0ecb525f (observed prefix)
Lets the service connect later shopping activity to the same browser installation.
- Browser namechrome
Adds device context to the installation record.
- Extension version3.0.2.2
Shows which Lumaly build made the request.
- Shop hostnameamazon.de (illustrative supported-shop domain)
Connects your supported-shop page context to the same installation identifier.
- Shopping actionapi/actions/visits (code path)
Links coupon, visit, purchase, or shop-list activity to the same stored identifier.
The same stored lead_id is created once and reused in later API calls
Install request obtains the lead ID
background.jsLeadApi.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.jsCouponsApi.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.jsShopsApi.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.jsStatsApi.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] } })) })) }- api.lumaly.de
Receives the install-ID request and later coupon, shop, stats, transaction, and account API requests that include lead_id.