Skip to content

1.4.1 introduces undisclosed telemetry: merchant identity resolved from API key and transmitted externally on every SDK init, no opt-out provided #217

Description

@danieldanzigerupwind

Description

Version 1.4.1 introduces undocumented telemetry in utils/logger.js that fires automatically on every SDK initialization. There is no opt-out, no disclosure in the README, and no meaningful changelog entry describing the privacy implications.

On every init() call, the SDK silently:

  • Sends the user's Flutterwave public API key as a URL query parameter to an external endpoint
  • Uses that key to resolve the merchant's real business name
  • Transmits the resolved business name to a Flutterwave-controlled telemetry server
  • Writes an SDK state file to the system temp directory

This is not a malicious finding- the infrastructure (f4b-flutterwave.com) is Flutterwave-controlled. The concern is that this behavior is entirely undisclosed and users have no way to consent to or disable it.

Steps to Reproduce

  1. Install flutterwave-node-v3@1.4.1
  2. Call any SDK method that triggers init(publicKey, libraryVersion) — this fires automatically on SDK use
  3. Observe two outbound HTTPS requests: one to api.ravepay.co, one to signozservice-prod.f4b-flutterwave.com
  4. Observe a new file created at os.tmpdir()/.flw_sdk.json

Expected behaviour

SDK initialization performs only the operations documented in the README. Any telemetry or analytics collection is disclosed upfront and can be disabled.

Actual behaviour

1. Public API key sent as a URL query parameter

The user's Flutterwave public key is embedded directly in the URL of a GET request, where it will appear in server access logs, reverse proxy logs, and any network monitoring in the user's environment:

// utils/logger.js
const query = `PBFPubKey=${encodeURIComponent(publicKey)}`;
const options = {
  hostname: 'api.ravepay.co',
  path: `/flwv3-pug/getpaidx/api/mercinfo?${query}`,  // key visible in URL
  method: 'GET',
};

2. Merchant business name resolved and transmitted without consent

The SDK uses the API key to look up the merchant's real business name, then ships it to the telemetry endpoint- going well beyond what SDK analytics would reasonably require:

// utils/logger.js
const name = data?.mn || null;  // merchant's real business name

return _post({
  name: 'app.created',
  data: {
    public_key: merchantName,  // business name transmitted to f4b-flutterwave.com
    library: LIBRARY,
    library_version: libraryVersion,
  },
});

3. No opt-out mechanism

init() fires unconditionally with no environment variable, configuration flag, or any other way to disable it:

// utils/logger.js
function init(publicKey, libraryVersion = DEFAULT_LIBRARY_VERSION) {
  if (_initPromise) return _initPromise;  // only prevents double-init, not disabling

  _initPromise = _fetchMerchantName(publicKey)   // always executes
    .then((merchantName) => {
      return _post({ name: 'app.created', ... }); // always executes
    })

4. Hardcoded telemetry API key committed in source

A fallback API key for the telemetry service is hardcoded in plaintext and is now publicly readable in the npm tarball:

// utils/logger.js
const API_KEY =
  process.env.SIGNOZ_API_KEY || 'IuUnO5cwI6Ta1JO/LEFUsMyz1AH3FNzW';

Anyone who reads the source can use this key to post arbitrary events to the telemetry endpoint.

5. Silent disk writes to system temp directory

The SDK writes a persistent state file to os.tmpdir() on every initialization, with no documentation and no cleanup:

// utils/logger.js
const STORE_PATH =
  process.env.FLW_SDK_STATE_PATH || path.join(os.tmpdir(), '.flw_sdk.json');

function _saveAppId(publicKey, appId) {
  const store = _readStore();
  store[_keyHash(publicKey)] = {
    app_id: appId,
    saved_at: new Date().toISOString(),
  };
  fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), 'utf8');
}

This will fail silently in read-only container filesystems and persists indefinitely with no documented cleanup path.

Reproduces how often

fires on every fresh SDK initialization where the in-memory and disk caches are empty (i.e. first run per environment).

Configuration

  • API Version: v3
  • Environment: test mode and live mode (fires in both; key prefix determines which)
  • Browser: N/A
  • Language: Node.js

Additional Information

The CHANGELOG entry for 1.4.1 reads:

Refactor Signoz integration.

  • [UPDATED] Persist merchant name across different event logs.

There is no mention of outbound network calls, identity resolution from the API key, or filesystem writes. Users reading the changelog before upgrading have no way to understand the privacy implications of this version.

Suggested fixes:

  1. Add an opt-out at the top of init():
    if (process.env.FLW_DISABLE_TELEMETRY === '1') return Promise.resolve();
  2. Document the telemetry behavior in the README — what is collected, where it is sent, and how to disable it
  3. Switch the mercinfo call from GET to POST with a JSON body, to keep the API key out of URL logs
  4. Remove the hardcoded SIGNOZ_API_KEY fallback value from source
  5. Document the os.tmpdir()/.flw_sdk.json file and provide a cleanup path or TTL

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions