Is PriceBlink Coupons and Price Comparison safe?

High risk

PriceBlink runs retailer configuration from tb.priceblink.com as page-context JavaScript during coupon autofill.

During auto-coupon flows, the extension fetches retailer-specific instructions from tb.priceblink.com/retailer.php and injects those strings into the page as script alongside page content and form fields. Its content scripts also accept openUrl and openCloseUrl window messages on all HTTP and HTTPS pages without an origin check, allowing web pages to make the extension open background tabs.

PriceBlinkv9.17Chrome Web Store
72Risk
Who publishes it

CNET Media, Inc. - 1 other listing from the same operator, none carrying a finding

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

Publisher
PriceBlink
Declared legal entity
CNET Media, Inc.
Registered address
114 5th Ave fl 15, New York, NY 10011-5610, US
Registered contact
Karl Quist

Same operator - 1 listing

Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.

Shared hosts - 3 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.

priceblink.com
Also called by 1 other listing: PriceBlink - Price Comp & Coupons
tb.priceblink.com
Also called by 1 other listing: PriceBlink - Price Comp & Coupons
tb.getinvisiblehand.com
Also called by 4 other listings, including PriceBlink - Price Comp & Coupons

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

Publishers can request a review.

Findings

Retailer config runs as checkout-page JavaScript

PriceBlink fetches retailer config from tb.priceblink.com for auto-coupon support.

Several response fields feed runEval, which builds and appends a script element to the page head, letting retailer code touch checkout DOM and coupon inputs.

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

You start an auto-coupon flow on a supported checkout page.

The extension has already requested retailer-specific coupon settings for that site.

The extension did this

The extension runs selected retailer-configuration fields as page JavaScript.

The generated script is appended to the page head, so it executes in the page context during coupon application.

Captured request
GEThttps://tb.priceblink.com/retailer.php?rid=35&auto_coupon_support=1

JSON retailer configuration was returned; the recorded evidence did not include a response body.

Retailer configuration fields that are executed during coupon flows
  • Before applying coupons
    ac_pre_apply_codes

    Lets the retailer response run code before coupon testing starts on the checkout page you are viewing.

  • Before filling the coupon input
    ac_pre_input

    Lets the retailer response run code immediately before the extension writes a coupon into the page input.

  • Before submitting the coupon
    ac_pre_submit

    Lets the retailer response run code while the checkout form is ready to be submitted.

  • Submit fallback code
    coupon_submit_selector

    If the configured submit selector cannot be clicked normally, the same configured value is executed as page JavaScript.

  • After submitting the coupon
    ac_post_submit

    Lets the retailer response run code after each coupon submission step.

The code that does this

The fetched retailer object feeds page-context script injection

Readable version

Background fetch for retailer configuration

js/background.js
async function getRetailerByID_async(rid) {  removeExpiredSuppressedRetailers();  const response = await fetch("https://tb.priceblink.com/retailer.php?uid=" + uid + "&browser=" + currentBrowserType + "&ver=" + ver + "&partnerid=0&rid=" + rid + "&auto_coupon_support=1", {    method: "GET",    headers: {      "x-cdn-debug": "fastly"    }  });  const data = await response.json();  return data}

Coupon flow executes ac_pre_apply_codes

js/app.js
async function applyCode(ret, code, index, couponsAmount) {  let acContainer = window.shadow.querySelector("#priceblink-ac-container");  if (!acContainer) {    injectAutoCouponsPopup("ac-apply-coupons", (function() {      updateStatus(code, index, couponsAmount);      applyCode(ret, code, index, couponsAmount)    }));    return  }  if (ret.act_bypass_transition_logic == "0") {    if (ret.act_pre_apply_codes) {      const inputEL = $(ret.act_coupon_input_selector);      const preApplyEL = $(ret.act_pre_apply_codes);      if (preApplyEL.length > 0) {        if (inputEL.length == 0 || inputEL.is(":hidden")) {          try {            preApplyEL[0].click()          } catch (e) {            console.warn(e)          }          await setTimeoutAsync(ret.act_preapply_delay)        }      }    }  } else {    if (ret.ac_pre_apply_codes) {      runEval(ret.ac_pre_apply_codes)    }  }  setTimeout((async function() {    updateStatus(code, index, couponsAmount);    if (ret.act_bypass_transition_logic == "0") {      try {        console.log("MV3");        fillAndSubmitCodeV3(ret, code)      } catch (e) {        console.warn(e);        ret.act_bypass_transition_logic = "1";        console.log("Fallback to MV2");        fillAndSubmitCodeV2(ret, code)      }    } else {      console.log("MV2");      fillAndSubmitCodeV2(ret, code)    }    await sendMessagePromise({      action: "applyNextCode",      url: document.location.href    });    applyAllCodes(ret)  }), ret.ac_submit_delay * 1e3)}

Coupon input and submit hooks call runEval

js/app.js
function fillAndSubmitCodeV2(ret, code) {  if (ret.ac_pre_input) {    runEval(ret.ac_pre_input)  }  $(ret.coupon_input_selector)[0] && $(ret.coupon_input_selector).val(code);  if (ret.ac_pre_submit) {    runEval(ret.ac_pre_submit)  }  try {    $(ret.coupon_submit_selector)[0] && $(ret.coupon_submit_selector)[0].click()  } catch (e) {    runEval(ret.coupon_submit_selector)  }  if (ret.ac_post_submit) {    runEval(ret.ac_post_submit)  }}

runEval appends generated script into the page head

js/app.js
function runEval(code) {  let result = document.createElement("div");  result.id = "pb-autocoupons-eval-result";  document.body.appendChild(result);  let callback = function(mutationsList) {    for (let mutation of mutationsList) {      if (mutation.attributeName === "result") {        observer.disconnect();        result.remove();        script.remove();        break      }    }  };  let observer = new MutationObserver(callback);  observer.observe(result, {    attributes: true  });  let script = document.createElement("script");  script.type = "text/javascript";  script.text = "(function () {\n    var result = null;\n    try {\n      result = JSON.stringify(function(){" + code + "}());\n    } catch(e) {\n      e.error = true;\n      result = JSON.stringify(e, ['message', 'name', 'error']);\n    }\n    document.getElementById('" + result.id + "').setAttribute('result', result);\n  }())";  document.getElementsByTagName("head").item(0).appendChild(script)}
Remote host involved in this code path
    • tb.priceblink.com

    Returns retailer configuration consumed by the PriceBlink background worker and auto-coupon content script.

Where it sends data

Destinations our analysis observed PriceBlink 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.

  • tb.priceblink.com

    PriceBlink sends data to tb.priceblink.com. One other extension we have analysed sends data here.

Updated 30 September 2026aoiidodopnnhiflaflbfeblnojefhigh