Is Rakuten: Get Cash Back For Shopping safe?

High risk

Code analysis indicates Rakuten captures the full HTML of your order confirmation pages and sends it to third-party servers.

When an order confirmation page loads, the extension reads the complete innerHTML of the page and POSTs it to capture.ecbsn.com and capture.fillr-tech.com. Requests include a hardcoded developer key. Order confirmation pages typically contain itemized purchase details, shipping addresses, and personal identifiers. This behavior has not been directly verified through dynamic analysis.

Rakuten Ebatesv26.18.1Chrome Web Store
75Risk
Who publishes it

Ebates Performance Marketing Inc., d/b/a Rakuten Rewards - no other listings under this identity, 8 shared hostnames

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

Publisher
Rakuten Ebates
Declared legal entity
Ebates Performance Marketing Inc., d/b/a Rakuten Rewards
Registered address
800 Concar Drive, 5th Floor, San Mateo, CA 94402, US

Shared hosts - 8 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.fillr.com
Also called by 2 other listings, including Rakuten: Get Cash Back For Shopping
button.rrcbsn.com
Also called by 2 other listings, including Rakuten: Get Cash Back For Shopping
cas.rrcbsn.com
Also called by 2 other listings, including Rakuten: Get Cash Back For Shopping
search.ecbsn.com
Also called by 2 other listings, including Rakuten: Get Cash Back For Shopping
static.ebates.com
Also called by 2 other listings
static.rakuten.com
Also called by 2 other listings
verishop.com
Also called by 2 other listings
capture.ecbsn.com
Also called by 6 other listings, including Rakuten Button Canada: Get Cash Back for Shopping, 楽天リーベイツ ポイントアシスト

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Rakuten Uploads Full Order Page HTML to Two Third-Party Servers

During an active shopping trip, a purchase makes the extension read the order confirmation HTML (name, address, items, total) and upload it to capture.ecbsn.com and capture.fillr-tech.com via a hardcoded shared API key.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You complete a purchase on any merchant site where you activated a Rakuten shopping trip.

The trigger requires an active shopping trip tracking number (TTN), meaning Rakuten was already involved in tracking your browsing session at that merchant.

The extension did this

Rakuten reads the entire HTML of your order confirmation page and uploads it to two third-party servers.

Your order details, including name, shipping address, items, and total, are packaged as gzip-compressed JSON and sent to both Rakuten's analytics backend and a Fillr data capture service using a hardcoded developer key.

02EvidenceFIELD TABLE
Data included in the order confirmation page upload
FieldValueWhy it matters
Full order page HTML
<!DOCTYPE html><html><head>...</head><body>...Order #A1B2C3D4 confirmed. Shipping to Jane Smith, 42 Maple St, Portland OR 97201...</body></html>The full HTML of your checkout confirmation page: name, delivery address, items, quantities, prices, order number.
Order confirmation URL
https://www.amazon.com/gp/buy/thankyou/handlers/display.html?ie=UTF8&checkoutSessionId=abc123The full URL of the order confirmation page, sent as content_source in the upload payload.
Rakuten Member ID
rr_7482910Your Rakuten account identifier, included in the ecbsn.com upload payload as rr_member_id.
Store Name and ID
Amazon / store_id: 1234The merchant name and internal Rakuten store identifier, included in both upload payloads.
03EvidenceCODE COMPARE
The code that does this

merchant.js: page classifier triggers innerHTML capture

What it actually does
Trigger in content/merchant.js (annotated)js/content/merchant.js
// Check feature flag (OCPDOM_CAPTURE_EABLED — note: typo in source)
const captureEnabled = featureFlags.isEnabled(FLAGS.OCPDOM_CAPTURE_EABLED);

// Fire only when: page is an order confirmation, capture not yet done,
// and an active Rakuten shopping trip tracking number exists
if (pageType === 'OrderConfirm' && captureEnabled &&
    !this.OCPDOMCaptured && this.merchant.session.shoppingTrip.ttn) {

  // Capture the ENTIRE page HTML
  const html = document.documentElement.innerHTML;

  if (html) {
    // Fire event to background service worker
    sendEvent('OCPDOMCapture', {
      content: html,          // full page HTML
      merchant: this.merchant,
      detail: pageType
    });
    this.OCPDOMCaptured = true;  // prevent duplicate sends
  }
}
bg.js: dual POST handler (annotated)js/bg.js
// Upload #1: POST to Rakuten analytics backend (ecbsn.com)
handleCaptureOrderPage({ url, tabId, data }) {
  const { storeName, storeId, session } = data.merchant;
  this.postData(this.deps.captureOrderUrl, {}, {
    tenant_id: this.deps.tenantId,
    source_name: 'toolbar',
    store_name: storeName,
    store_id: storeId,
    shopping_trip_id: session.shoppingTrip.ttn,
    rr_member_id: this.deps.getMemberId(),  // Rakuten account ID
    content_source: url,                    // order confirmation URL
    content_type: 'order confirmation',
    content_format: 'html',
    content: data.content                   // full page HTML
  });
}

// Upload #2: POST to Fillr third-party capture (fillr-tech.com)
handleOCPDOMCapture({ url, tabId, data }) {
  const ebToken = this.deps.getEBToken();
  if (this.deps.fillrOCPDOMUploadUrl && ebToken) {
    const headers = {
      'x-dev-key': this.deps.domUploadDevKey,  // hardcoded: f401eb0b18d9ca5c20ad6e16574b0e27
      ebtoken: ebToken
    };
    this.postData(this.deps.fillrOCPDOMUploadUrl, headers, {
      tracking_ticket: session.shoppingTrip.ttn,
      content: data.content  // same full page HTML
    });
  }
}

// Both uploads use gzip compression
async postData(url, extraHeaders = {}, payload) {
  const compressed = await gzip(JSON.stringify(payload));
  return await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Encoding': 'gzip',
      ...extraHeaders
    },
    body: compressed
  });
}
04EvidenceTHIRD PARTY LIST
Both servers receive a copy of your order confirmation HTML
  • capture.ecbsn.com

    Rakuten's order capture analytics backend (Rakuten/Ebates). Receives full page HTML plus member ID, shopping trip ID, store details, and order URL.

  • capture.fillr-tech.com

    Fillr's third-party data capture service. Receives a copy of the order page HTML via a hardcoded developer key. Fillr is an autofill/data company; appears to be a data partnership.

05EvidenceARTIFACT
Reproduce it yourself

Reads the Rakuten extension source files and confirms all three components of the order capture mechanism are present: the innerHTML trigger in merchant.js, the dual-POST handler in bg.js, and the hardcoded Fillr dev key.

RequiresNode.js 18+
rakuten-ocp-capture-verify.js · js
'use strict';

const fs = require('fs');
const path = require('path');

const EXT_DIR = process.argv[2] || './extracted';

const bgContent = fs.readFileSync(path.join(EXT_DIR, 'js/bg.js'), 'utf8');
const merchantContent = fs.readFileSync(path.join(EXT_DIR, 'js/content/merchant.js'), 'utf8');

const extract = (content, pattern) => {
  const m = content.match(pattern);
  return m ? m[1] : '(NOT FOUND)';
};

const CAPTURE_ORDER              = extract(bgContent, /CAPTURE_ORDER:"([^"]+)"/);
const FILLR_OCP_DOM_UPLOAD_DEVKEY = extract(bgContent, /FILLR_OCP_DOM_UPLOAD_DEVKEY:"([^"]+)"/);
const FILLR_OCP_DOM_UPLOAD_URL    = extract(bgContent, /FILLR_OCP_DOM_UPLOAD_URL:"([^"]+)"/);

const hasHandleCaptureOrderPage  = bgContent.includes('handleCaptureOrderPage');
const hasHandleOCPDOMCapture     = bgContent.includes('handleOCPDOMCapture');
const hasGzip                    = bgContent.includes('Content-Encoding') && bgContent.includes('gzip');
const hasDevKeyHeader            = bgContent.includes('x-dev-key');
const hasInnerHTMLCapture        = merchantContent.includes('document.documentElement.innerHTML');
const hasOrderConfirmTrigger     = merchantContent.includes('OrderConfirm');

const EXPECTED_DEV_KEY = 'f401eb0b18d9ca5c20ad6e16574b0e27';
const keyMatch = FILLR_OCP_DOM_UPLOAD_DEVKEY === EXPECTED_DEV_KEY;

console.log('=== Rakuten Claim 1122: Order Confirmation Page HTML Capture ===\n');
console.log('Hardcoded endpoints extracted from js/bg.js:');
console.log('  CAPTURE_ORDER:               ' + CAPTURE_ORDER);
console.log('  FILLR_OCP_DOM_UPLOAD_URL:    ' + FILLR_OCP_DOM_UPLOAD_URL);
console.log('  FILLR_OCP_DOM_UPLOAD_DEVKEY: ' + FILLR_OCP_DOM_UPLOAD_DEVKEY);
console.log('  Dev key matches expected:    ' + keyMatch);
console.log();
console.log('Handler presence in js/bg.js:');
console.log('  handleCaptureOrderPage: ' + hasHandleCaptureOrderPage);
console.log('  handleOCPDOMCapture:    ' + hasHandleOCPDOMCapture);
console.log('  gzip encoding:          ' + hasGzip);
console.log('  x-dev-key header:       ' + hasDevKeyHeader);
console.log();
console.log('Trigger in js/content/merchant.js:');
console.log('  innerHTML capture:       ' + hasInnerHTMLCapture);
console.log('  OrderConfirm classifier: ' + hasOrderConfirmTrigger);
console.log();

if (keyMatch && hasHandleCaptureOrderPage && hasHandleOCPDOMCapture && hasInnerHTMLCapture) {
  console.log('RESULT: CONFIRMED — all claim components present.');
  console.log('  1. merchant.js reads innerHTML on OrderConfirm pages with active TTN.');
  console.log('  2. bg.js POSTs HTML to ' + CAPTURE_ORDER);
  console.log('  3. bg.js POSTs same HTML to ' + FILLR_OCP_DOM_UPLOAD_URL);
  console.log('     using hardcoded dev key: ' + FILLR_OCP_DOM_UPLOAD_DEVKEY);
  console.log('  4. Both uploads are gzip-compressed JSON.');
} else {
  console.log('RESULT: INCONCLUSIVE — one or more components not found.');
}
How to run it
  1. 1
    node rakuten-ocp-capture-verify.js /path/to/chhjbpecpncaggjpdakmflnfcopglcmi/extracted
SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Merchant site visits sent to two Rakuten analytics endpoints on every navigation

Navigating to any site on Rakuten's tracked merchant list sends your hostname and a session ID to Segment (events.engager.ecbsn.com) and messaging (api.rakuten.com).

Six POSTs captured across three sites; Segment uses a hardcoded key.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to a website on Rakuten's merchant tracking list, such as amazon.com, walmart.com, or ebay.com.

The extension did this

The extension sends your hostname to two Rakuten-controlled batch endpoints: a Segment analytics endpoint and a messaging endpoint.

This fires once per domain per 30-minute window. No shopping trip or click-through is required.

02EvidenceFIELD TABLE
What the Segment endpoint (events.engager.ecbsn.com/v1/batch) receives on each navigation:
FieldValueWhy it matters
Merchant hostname
amazon.comThe domain of the merchant site you are visiting, for example amazon.com.
Toolbar ID
465221942A numeric identifier assigned to your Rakuten extension install, used to link events back to your account.
Anonymous ID
a3f71b2c-84de-4e9c-bc12-3f9a11e70d55A persistent session identifier that Rakuten uses to correlate browsing events across navigations.
User agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36Your browser name and version string, used for device fingerprinting.
Extension version
5.1.0The version of the Rakuten extension installed.
03EvidenceFIELD TABLE
Data received by Rakuten's messaging endpoint (messages/batch)
FieldValueWhy it matters
Page URL
amazon.com/The full URL of the merchant page you visited.
Page host
amazon.comThe hostname of the page you are on.
Installation ID
b2e44a91-3f87-4c01-a812-9e3c5d70b124A persistent identifier tied to your extension installation.
User agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36Your browser name and version string.
04EvidenceNETWORK CAPTURE
Captured request
POSThttps://events.engager.ecbsn.com/v1/batch
200 OK (empty body). Three identical POST requests captured during dynamic analysis: one each for amazon.com, walmart.com, ebay.com.
Headers
Content-Typeapplication/json
AuthorizationBasic UlJrenJjdEV3dG1ndDhQZ1lXa0Y2U3pONjJvYUFpSUQ6
Body
{
  "batch": [
    {
      "type": "track",
      "event": "Visit NP Page",
      "properties": {
        "host": "amazon.com"
      },
      "context": {
        "browser_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        "app_version": "5.1.0",
        "browser": "chrome",
        "toolbarid": 465221942
      },
      "anonymousId": "a3f71b2c-84de-4e9c-bc12-3f9a11e70d55",
      "writeKey": "RRkzrctEwtmgt8PgYWkF6SzN62oaAiID",
      "sentAt": "2026-04-18T13:29:47.212Z"
    }
  ],
  "sentAt": "2026-04-18T13:29:47.213Z"
}
05EvidenceCODE COMPARE
The code that does this

The code that fires both tracking calls on every matched merchant navigation:

What it actually does
Domain check and dual-track call
// Fires on every handleDocumentComplete (navigation) event.
async function trackNonPartnerPage(url) {
  const domain = parseDomain(url)?.domain;
  if (!domain) return;
  if (!settings.get('trackNP')) return;  // feature flag must be enabled
  if (!self.EBATES) return;

  // Lazy-load tracked domain list from remote config
  if (!domainSet) domainSet = await DomainList.getDomains();
  if (!domainSet.has(domain)) return;

  // Dedup: skip if already sent within the last 30 minutes
  if (!lastVisitMap) lastVisitMap = await storage.get('np-domains-last-visit') || new Map();
  lastVisitMap.forEach((ts, d) => {
    if (ts + 1_800_000 < Date.now()) lastVisitMap.delete(d);  // expire old entries
  });

  if (!lastVisitMap.has(domain)) {
    // Send to Segment analytics
    EBATES.segment.track('Visit NP Page', { host: domain });

    // Send to Rakuten holistic messaging
    const fullUrl = `https://${domain}`;
    EBATES.holistic.track(Events.WebsiteVisited, {
      data: { website_visit_id: generateUUID() },
      url: fullUrl
    });
  }

  lastVisitMap.set(domain, Date.now());
  await storage.set('np-domains-last-visit', lastVisitMap);
}
Segment batch POST with hardcoded write key
// The Segment analytics client, configured with a hardcoded write key.
const segmentClient = new SegmentAnalytics({
  key: config.get('SEGMENT_KEY'),    // 'RRkzrctEwtmgt8PgYWkF6SzN62oaAiID'
  host: config.get('SEGMENT_HOST'),  // 'https://events.engager.ecbsn.com'
});

// The POST that transmits queued events:
fetch(`${this.host}/v1/batch`, {
  method: 'POST',
  headers: {
    Authorization: `Basic ${btoa(this.writeKey + ':')}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(batch),
});
06EvidenceTHIRD PARTY LIST
Where your browsing data ends up:
  • events.engager.ecbsn.com

    Rakuten's self-hosted Segment analytics endpoint. Receives 'Visit NP Page' events including merchant hostname, toolbar ID, and anonymous ID. ecbsn.com is a Rakuten-owned domain.

  • api.rakuten.com

    Rakuten's core API. Receives 'Website Visited' holistic events including page.url, page.host, and installation ID via the /message/v2/regions/USA/messages/batch path.

SeverityLOW
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Failed Cash-Back Activations Logged to a Third-Party Datadog Account

When a Cash Back activation is interrupted (closed tab, timeout, back/forward nav, or page error), the extension logs the failure to its own analytics and to Datadog, including store ID, redirect chain, and user agent, via a hardcoded key.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You click a Cash Back activation link and the merchant redirect is interrupted: it times out, you close/reload the tab, navigate back/forward, or the destination errors.

This is a common, everyday interaction, no unusual behavior on your part is required.

The extension did this

The extension records the failed activation to its own analytics and separately to a third-party Datadog account, including your user agent and the merchant's store ID.

This is a warn-level event named 'ActivationStatus', delivered in a batched POST alongside any other queued log entries.

02EvidenceFIELD TABLE
What the Datadog log entry for a failed cash-back activation includes:
FieldValueWhy it matters
Merchant store ID
1234Rakuten's internal identifier for the merchant you were trying to buy from.
Affiliate redirect URLs
https://click.linksynergy.com/deeplink?id=Xa1Bc2De3Fg&mid=1234&murl=https%3A%2F%2Fwww.nike.com%2F,https://www.nike.com/The full chain of tracking URLs the extension routed you through on the way to the merchant, joined into one string.
Failure reason
closed tabWhy the activation didn't complete, tab closed, redirect timed out, page navigated back, or a page error.
Browser user agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36Your browser name, version, and OS string, used for device identification.
Extension page URL
chrome-extension://chhjbpecpncaggjpdakmflnfcopglcmi/The address of the extension's own internal page, sent as the log's 'view.url' field on every entry.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://browser-intake-datadoghq.com/api/v2/logs?ddsource=browser&ddtags=api%3Afetch%2Cdatacenter%3Aus&dd-api-key=pub5c8940d1d60d49fa1ad47b86997d8ed9&dd-evp-origin=browser&dd-request-id=3f9a11e7-0d55-4b21-9c3f-71b284de4e9c
200 OK (empty body). Observed during dynamic analysis: an organic 'ActivationStatus' warn-level log fired to this endpoint during normal browsing, carrying http.useragent and view.url exactly as coded. A supplementary forced-error request confirmed the same delivery pipeline handles both business-logic warnings and unhandled exceptions.
Headers
Content-Typeapplication/json
Body
[
  {
    "ddsource": "browser",
    "message": "ActivationStatus",
    "status": "warn",
    "date": 1755298187212,
    "http": {
      "useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
    },
    "origin": "logger",
    "view": {
      "referrer": "chrome-extension://chhjbpecpncaggjpdakmflnfcopglcmi/",
      "url": "chrome-extension://chhjbpecpncaggjpdakmflnfcopglcmi/"
    },
    "service": "rr-button",
    "store_id": "1234",
    "redirect_chain": "https://click.linksynergy.com/deeplink?id=Xa1Bc2De3Fg&mid=1234&murl=https%3A%2F%2Fwww.nike.com%2F,https://www.nike.com/",
    "reason": "closed tab"
  }
]
04EvidenceCODE COMPARE
The code that does this

bg.js builds the activation-failure log payload; DdLogger batches and POSTs it to Datadog

What it actually does
Activation-failure handler (annotated)js/bg.js
// Called with a reason of 'timed out', 'closed tab', 'reload tab',
// 'forward_back', or 'page error' whenever an affiliate redirect
// doesn't complete cleanly.
function handleActivationFailure({ tabId, reason, extra }) {
  const tab = pendingActivations.get(tabId);
  if (!tab) return;

  const storeId = tab.storeId;
  const merchant = self.EBATES.merchants.get(storeId);
  const trip = merchant?.session.shoppingTrip.getCurrentState();

  const payload = {
    store_id: storeId,
    redirect_chain: tab.urls.toString(),   // every URL in the redirect hop
    reason,
    ...extra,
    ...(trip ? {
      activation_type: trip.type,
      attribution_source_previous_click: trip.source,
      tracking_ticket: trip.ttn,
    } : {}),
  };

  // Sent to Rakuten's own Segment pipeline...
  self.EBATES.segment.track('Activation Failure', payload);
  // ...AND to the third-party Datadog logger, same payload.
  ddLogger.warn('ActivationStatus', payload);

  self.EBATES.settings.affiliates.clear(tabId);
}
DdLogger: batches the entry with device/page context, then POSTs to Datadog (annotated)js/bg.js
class DdLogger {
  globalContext = {};
  batch = [];

  warn(message, fields = {}) {
    return this.addToBatch(message, fields, 'warn');
  }

  async addToBatch(message, fields, status) {
    if (this.disabled) return;
    const entry = {
      ...fields,
      ...this.globalContext,
      ddsource: 'browser',
      message,
      date: Date.now(),
      http: { useragent: navigator.userAgent },   // added to EVERY log entry
      status,
      origin: 'logger',
      view: {
        referrer: `${self.location.origin}/`,     // the extension's own page URL
        url: `${self.location.origin}/`,
      },
    };
    // ddIgnoreList can suppress specific messages; ActivationStatus is not on it
    this.batch.push(entry);
    await this.fetching;
    this.send();
  }

  fetch(batch) {
    return fetch(
      buildUrl('https://browser-intake-datadoghq.com/api/v2/logs', {
        ddsource: 'browser',
        ddtags: 'api:fetch,datacenter:us',
        'dd-api-key': config.get('DATADOG_TOKEN'),  // hardcoded, same for every install
        'dd-evp-origin': 'browser',
        'dd-request-id': generateRequestId(),
      }),
      { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(batch) }
    );
  }
}
05EvidenceTHIRD PARTY LIST
Where the failure log ends up:
  • browser-intake-datadoghq.com

    Datadog's log-ingestion endpoint. Receives 'ActivationStatus' with your user agent, page URL, store ID, and redirect chain, separate from Rakuten's own analytics.

+1 more finding not shown

Where it sends data

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

  • capture.ecbsn.com

    Rakuten sends data to capture.ecbsn.com. 2 other extensions we have analysed send data here.

  • capture.fillr-tech.com

    Rakuten sends data to capture.fillr-tech.com. One other extension we have analysed sends data here.

Updated 30 September 2026chhjbpecpncaggjpdakmflnfcopglcmi