Is Mojo - Automatic Coupons at Checkout safe?
Mojo runs JavaScript function strings fetched from its own server inside every website you visit.
On every http/https page, Mojo asks api.mojocart.com for that merchant's coupon-testing configuration, which can include 'func' entries containing raw JavaScript. The extension injects a script into the page itself and runs those server-supplied functions with new Function(), giving them the same access to the page's DOM, cookies, and local storage as the site's own code. Separately, the coupon codes you type, the checkout page URL, and cart/price details it extracts are sent to Mojocart's backend on every coupon submission and price check.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Mojocart's remote store config can run vendor code on checkout pages
Code analysis shows that on stores Mojocart supports, this extension fetches config from Mojocart's server and, if it carries a function callback, runs that code directly in the page.
We did not observe it firing in our test session.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Static analysis
You open a checkout page on a store that Mojocart supports.
Content scripts run on every http(s) page except a short denylist.
The extension fetches that store's config from Mojocart's server and, if a callback is present, runs it directly in the page.
The callback runs with jQuery and page data in scope, with the page's own DOM and cookie access.
The message handler that turns a config string into running code
// scripts/executor.js -- window "message" listener, runs in the page's MAIN worldwindow.addEventListener("message", (event) => { const { origin, source: senderWindow, data: { type, data: msg = {} } = {} } = event; // Same-document check only -- NOT an allowlist of trusted senders. if (origin === window.location.origin && type === "FUNCTION_EXECUTOR_REQUEST") { const { funcStr, envVariables = {}, envVariableKeys = [], key } = msg; const result = (() => { try { const env = sanitizeEnvKeys(envVariables, envVariableKeys); // Builds and RUNS the server-supplied function body, with jQuery // and the env values in scope, inside the page's own JS context. return { result: new Function("$", ...Object.keys(env), funcStr)( jQuery, ...Object.values(env) ) }; } catch (err) { return { error: err.message, result: undefined }; } })(); senderWindow.postMessage( { type: "FUNCTION_EXECUTOR_RESPONSE", data: { key, funcStr, ...result } }, origin ); }}, false);- Callback kindfunc
Whether the entry is a fixed value, a DOM read, or code to run
- Callback codereturn document.querySelector('#promo-code').value
The JavaScript the extension passes to new Function() and executes
- Where it runscheckout.examplestore.com
Directly in the visited page, with that page's DOM and cookie access
Scans a captured get_store_config response for 'func' callback entries and prints the code each one would run.
- Node.js 14+
#!/usr/bin/env node/** * mojocart-config-func-scanner.js * * Scans a captured Mojocart /get_store_config JSON response and reports * any 'func' callback entries -- the config-driven code path that * scripts/executor.js runs with new Function() in the visited page. * * Usage: node mojocart-config-func-scanner.js response.json */const fs = require("fs");const file = process.argv[2];if (!file) { console.error("Usage: node mojocart-config-func-scanner.js <response.json>"); process.exit(1);}const config = JSON.parse(fs.readFileSync(file, "utf8"));const CALLBACK_LISTS = [ "code_testing_config_list", "cart_config_list", "checkout_input_config_list"];const CALLBACK_KEYS = [ "code_input_triggered_callbacks", "code_applied_callbacks", "code_removed_callbacks", "code_testing_finished_callbacks"];let found = 0;for (const listKey of CALLBACK_LISTS) { const entries = config[listKey]; if (!Array.isArray(entries)) continue; for (const entry of entries) { for (const key of CALLBACK_KEYS) { const callbacks = entry[key]; if (!Array.isArray(callbacks)) continue; for (const cb of callbacks) { if (cb && cb.type === "func") { found++; console.log(`[${listKey}.${key}] func callback found:`); console.log(cb.value); console.log("---"); } } } if (Array.isArray(entry.extractors)) { for (const extractor of entry.extractors) { const action = extractor.action; if (action && action.type === "func") { found++; console.log(`[${listKey}.extractors.action] func extractor found:`); console.log(action.value); console.log("---"); } } } }}console.log( found ? `${found} 'func' callback(s) found. Each one runs as new Function() in the visited page.` : "No 'func' callbacks in this response.");- 1Capture a get_store_config response body and save it as response.json.
- 2Run: node mojocart-config-func-scanner.js response.json.
- 3Any printed entry is code the extension will execute via new Function().
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.
Signed Mojocart config can execute a server-supplied function on any page
Code analysis shows every http(s) page this extension runs on can receive a signed config response from Mojocart naming a 'func' callback, which the extension then executes with new Function() in that page's own origin.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Static analysis
You load a page whose domain is on Mojocart's supported-store list.
matches is http://*/*, https://*/*, so almost any http(s) page qualifies.
content.js requests a signed store config from Mojocart; if it names a 'func' callback, the extension runs that code in the page.
Only a same-document origin check gates execution, not an allowlist of trusted senders.
Signing the request, then running what it returns
Signed request builder
background.js// background.js -- signs every get_store_config request before it leaves// the service worker. The signature proves the REQUEST came from this// extension; it says nothing about whether the RESPONSE is safe to run.async function fetchWithSignature(path, requestParams) { const { params, sign, tm } = await buildSignature(requestParams); const response = await httpGet(path, params, { baseURL: MOJOCART_API_BASE, headers: { "X-Signature": sign, "X-Timestamp": tm } }); return response.status === 200 ? parseStoreConfigResponse(response.data) : null;}Posting the callback to the executor
feature.js// feature.js -- posts a callback string to the page-world executor// and waits for its result via a matching window message.function callExecutor(funcStr, envVariables) { return new Promise((resolve) => { const requestKey = `${Date.now()}-${1000 + Math.floor(9000 * Math.random())}`; const onMessage = (event) => { const { origin, data: { type, data: msg } = {} } = event; const { funcStr: echoedFuncStr, result, error, key } = msg || {}; if ( origin === window.location.origin && type === "FUNCTION_EXECUTOR_RESPONSE" && echoedFuncStr === funcStr && key === requestKey ) { window.removeEventListener("message", onMessage, false); resolve(error ? undefined : result); } }; window.addEventListener("message", onMessage, false); window.postMessage( { type: "FUNCTION_EXECUTOR_REQUEST", data: { funcStr, envVariableKeys: Object.keys(envVariables), envVariables, key: requestKey } }, window.location.origin ); });}- Callback kindfunc
Schema-sanctioned values are a fixed value, a DOM read, or code to run
- Callback bodyreturn document.querySelector('#promo-code').value
The JavaScript source the extension executes via new Function()
- Request signatureX-Signature: 3f9c1a... X-Timestamp: 1758230400
Proves the request came from the extension; says nothing about the response
Reads the extension's manifest.json and prints exactly which http(s) pages the config-driven executor path can run on.
- Node.js 14+
#!/usr/bin/env node/** * mojocart-host-reach.js * * Reads the extension's own manifest.json and reports which pages the * content script -- and therefore the get_store_config-driven executor * path -- can run on. * * Usage: node mojocart-host-reach.js manifest.json */const fs = require("fs");const file = process.argv[2];if (!file) { console.error("Usage: node mojocart-host-reach.js <manifest.json>"); process.exit(1);}const manifest = JSON.parse(fs.readFileSync(file, "utf8"));const mainScript = (manifest.content_scripts || []).find( (cs) => Array.isArray(cs.js) && cs.js.includes("content.js"));if (!mainScript) { console.log("No content.js content_scripts entry found in this manifest."); process.exit(0);}console.log("Matches:", mainScript.matches);console.log("Excluded (exclude_globs):", mainScript.exclude_globs || []);console.log( "\nThe content script, and the get_store_config-driven executor path it " + "can trigger, loads on every http(s) page whose hostname is not one of " + "the excluded domains above.");- 1Point this at the extension's manifest.json.
- 2Run: node mojocart-host-reach.js manifest.json.
- 3It prints the matches/exclude_globs pair showing the executor path is not scoped to a merchant allowlist at the manifest level.
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 3.2.0. 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 every site you visit
http://*/*
Read and change your data on every secure site you visit
https://*/*
Read and change cookies, including the ones that keep you signed in
cookies
Store data in your browser
storage
Watch every request your browser makes
webRequest
Run its own code inside the pages you visit
scripting
Where it sends data
Destinations our analysis observed Mojo - Automatic Coupons at Checkout 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.
- api.mojocart.com
Mojo - Automatic Coupons at Checkout sends data to api.mojocart.com. No other extension we have analysed sends data here.