Is Copper CRM for Gmail and LinkedIn safe?

Medium risk

Copper CRM is medium risk. Copper's analytics path can send Segment POST requests after extension activity, built with your Copper user ID, event name, properties, browser context, and group traits. Network testing didn't trigger this path; no body was captured.

Copperv2.0.509Chrome Web Store
45Risk
Who publishes it

Copper CRM Inc. - no other listings under this identity, 2 shared hostnames

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

Publisher
Copper
Declared legal entity
Copper CRM Inc.
Registered address
2021 Fillmore St PMB2118, San Francisco, CA 94115, US
Registered contact
Copper CRM Inc.

Shared hosts - 2 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.copper.com
Also called by 3 other listings, including PandaDoc Proposals and eSignatures for Copper
app.copper.com
Also called by 6 other listings, including PandaDoc Proposals and eSignatures for Copper

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Segment Analytics Receives Copper Extension Events

Copper's analytics path can send Segment POST requests after extension activity, built with your Copper user ID, event name, properties, browser context, and group traits.

Network testing didn't trigger this path; no body was captured.

01EvidenceCAUSE EFFECT
What actually happens
You did this

Copper records an analytics event while you use the extension.

Examples in the code include page-view events, user-property updates, and group-property updates.

The extension did this

The background worker prepares a Segment analytics request with your Copper identifiers and event context.

Network testing did not capture a live request body for this path.

02EvidenceFIELD TABLE
Fields assembled for Segment analytics requests
FieldValueWhy it matters
Copper user ID
12345678 (illustrative)Ties analytics events to your Copper user account.
Event name
page_viewDescribes what you did in the extension or embedded Copper app.
Page URL
https://mail.google.com/mail/u/0/#inbox (illustrative)Can include the page or route associated with the analytics event.
Browser context
Chrome/126.0.0.0; chrome_extension 2.0.478 (illustrative)Adds your browser and extension version to the analytics record.
Group traits
internal_company_id: 98765 (illustrative)Can connect your activity to a Copper company or workspace record.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.segment.io/v1/track
Headers
Content-Typeapplication/json
AuthorizationBasic base64(segmentWriteKey + ":")
04EvidenceCODE COMPARE
The code that does this

Shipped bundle code that maps analytics messages to Segment POSTs

What it actually does
Readable message listener from bundled source mapbackground.map:message-listener.js
define("message-listener", ["exports", "./messages/ajax-proxy-request", "./messages/analytics", "./messages/dev-reload", "./messages/host-boot-ready", "./messages/runtime", "./messages/show-extension-options", "./messages/show-notification", "./messages/register-tab", "./messages/storage", "./messages/unsupported", "./messages/window-incognito", "./messages/invoke", "./messages/linkedin-awareness", "./messages/permissions-check", "./messages/permissions-request", "./messages/register-content-script", "./messages/test/sidepanel", "./messages/test/set-credentials", "./listener-util"], function (_exports, _ajaxProxyRequest, _analytics, _devReload, _hostBootReady, _runtime, _showExtensionOptions, _showNotification, _registerTab, _storage, _unsupported, _windowIncognito, _invoke, _linkedinAwareness, _permissionsCheck, _permissionsRequest, _registerContentScript, _sidepanel, _setCredentials, _listenerUtil) {
  "use strict";

  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.register = register;
  /**
   * Checks if the extension is running in a development environment
   * by checking if host_permissions contains 'copper.cool'
   * @returns {Boolean}
   */
  function isDevelopment() {
    const hosts = chrome.runtime.getManifest().host_permissions;
    const appRootUrl = hosts[0];
    return appRootUrl.includes('copper.cool');
  }

  /** @type {Object<string, BaseMessage>} */
  const _messageListenerMap = {
    'ajax.proxyRequest': new _ajaxProxyRequest.AjaxProxyRequest(),
    'analytics.addGroupProperties': new _analytics.AnalyticsGroup(),
    'analytics.addUserProperties': new _analytics.AnalyticsIdentify(),
    'analytics.logTrackedEvent': new _analytics.AnalyticsTrack(),
    'dev.reload': new _devReload.DevReload(),
    'host.boot.ready': new _hostBootReady.HostBootReady(),
    'invoke.contentScript': new _invoke.InvokeContentScript(),
    'invoke.sidePanel': new _invoke.InvokeSidePanels(),
    // single -> plural is intentional, as to content scripts it *appears* like a single side panel is messaged
    'invoke.everywhere': new _invoke.InvokeEverywhere(),
    'linkedin.awareness.trigger': new _linkedinAwareness.LinkedInAwareness(),
    'optionspage.show': new _showExtensionOptions.ShowExtensionOptions(),
    'runtime.id': new _runtime.RuntimeId(),
    'runtime.getURL': new _runtime.RuntimeGetURL(),
    'runtime.manifestVersion': new _runtime.RuntimeManifestVersion(),
    'notification.showNotification': new _showNotification.ShowNotification(),
    'permissions.check': new _permissionsCheck.PermissionsCheck(),
    'permissions.request': new _permissionsRequest.PermissionsRequest(),
    'scripting.registerContentScript': new _registerContentScript.RegisterContentScript(),
    'storage.local.get': new _storage.StorageLocalGet(),
    'storage.local.set': new _storage.StorageLocalSet(),
    'storage.local.remove': new _storage.StorageLocalRemove(),
    'register.tab': new _registerTab.RegisterTab(),
    'window.incognito': new _windowIncognito.WindowIncognito()
  };

  // Register development-only message handlers for CRX Playwright tests
  if (isDevelopment()) {
    _messageListenerMap['test.sidepanel.open'] = new _sidepanel.SidePanelOpen();
    _messageListenerMap['test.setCredentials'] = new _setCredentials.SetCredentials();
  }
  const _unsupportedListener = new _unsupported.Unsupported();
  function register() {
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      const {
        action,
        actionArgs
      } = message;
      const listener = _messageListenerMap[action] || _unsupportedListener;
      listener.execute(actionArgs, sender).then(response => {
        sendResponse(response);
      }).catch(error => {
        (0, _listenerUtil.handleListenerError)(sendResponse, error);
      });

      // Returning true here prevents Chrome from invalidating the `sendResponse`
      // function when this listener function completes.
      return true;
    });
  }
});
Readable Segment helper from bundled source mapbackground.map:messages/analytics.js
define("messages/analytics", ["exports", "./-base-message"], function (_exports, _baseMessage) {
  "use strict";

  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.AnalyticsTrack = _exports.AnalyticsIdentify = _exports.AnalyticsGroup = void 0;
  _baseMessage = _interopRequireDefault(_baseMessage);
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
  class SegmentApiHelper {
    #apiPath = 'https://api.segment.io/v1/';

    /**
     * Sends a POST request
     * @param {String} url fully qualified URL for the Segment endpoint
     * @param {Object} requestData  data payload that goes to Segment
     * @param {String} segmentWriteKey required to log data to the correct Segment source
     * @returns {Promise}
     */
    async #postSegmentApiRequest(url, requestData = {}, segmentWriteKey) {
      if (!segmentWriteKey) {
        throw new Error('No Segment writeKey available.');
      }

      // see: https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/
      const encodedWriteKey = btoa(`${segmentWriteKey}:`);
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Basic ${encodedWriteKey}`
        },
        body: JSON.stringify(requestData)
      });
      return response.json();
    }

    /**
     * Returns false if required Segment api data is not present
     * @param {Object} apiData payload containing required api dependencies
     * @return {Boolean}
     */
    #validateApiDependencies(apiData) {
      return apiData?.segmentWriteKey && apiData?.companyUserId;
    }

    /**
     * Send an group request to Segment. Group represents the entity of which the user is a member.
     * This correlates to the Company entity in Copper.
     * @param {Object} groupData
     * @param {Object} groupData.traits group specific data sent to Segment for logging
     * @param {Object} groupData.context platform and system specific data sent to Segment for logging
     * @param {Object} apiData required to make a call to Segment
     * @param {String} apiData.segmentWriteKey send user data to the correct Segment source
     * @param {String} apiData.companyUserId keeps userData associated to the correct Copper user
     */
    async group(groupData, apiData) {
      if (!this.#validateApiDependencies(apiData)) {
        throw new Error('group: API data not available.');
      }
      const {
        traits,
        context
      } = groupData;
      if (!traits?.internal_company_id) {
        throw new Error('group: internal_company_id is required.');
      }
      const segmentRequestBody = {
        userId: `${apiData.companyUserId}`,
        groupId: traits.internal_company_id,
        traits,
        context
      };
      return this.#postSegmentApiRequest(`${this.#apiPath}group`, segmentRequestBody, apiData.segmentWriteKey);
    }

    /**
     * Send an identify request to Segment
     * @param {Object} identifyData
     * @param {Object} identifyData.traits group specific data sent to Segment for logging
     * @param {Object} identifyData.context platform and system specific data sent to Segment for logging
     * @param {Object} apiData required to make a call to Segment
     * @param {String} apiData.segmentWriteKey send user data to the correct Segment source
     * @param {String} apiData.companyUserId keeps userData associated to the correct Copper user
     */
    async identify(identifyData, apiData) {
      if (!this.#validateApiDependencies(apiData)) {
        throw new Error('identify: API data not available.');
      }
      const {
        traits,
        context
      } = identifyData;
      const segmentRequestBody = {
        userId: `${apiData.companyUserId}`,
        traits,
        context
      };
      return this.#postSegmentApiRequest(`${this.#apiPath}identify`, segmentRequestBody, apiData.segmentWriteKey);
    }

    /**
     * Sends event data to Segment
     * @param {String} event name of the event sent to Segment
     * @param {Object} [properties] optional data payload sent with the event
     * @param {Object} context platform and system specific data sent to Segment for logging
     * @param {Object} apiData required to make a call to Segment
     * @param {String} apiData.segmentWriteKey send user data to the correct Segment source
     * @param {String} apiData.companyUserId keeps userData associated to the correct Copper user
     */
    async trackEvent(event, properties, context, apiData) {
      if (!this.#validateApiDependencies(apiData)) {
        throw new Error(`trackEvent: API data not available for [${event}].`);
      }
      const segmentRequestBody = {
        userId: `${apiData.companyUserId}`,
        event,
        properties,
        context
      };
      return this.#postSegmentApiRequest(`${this.#apiPath}track`, segmentRequestBody, apiData.segmentWriteKey);
    }
  }
  const segmentApiHelper = new SegmentApiHelper();
  class AnalyticsIdentify extends _baseMessage.default {
    /** @override */
    async execute(actionArgs, sender) {
      const {
        properties,
        apiData
      } = actionArgs;
      return segmentApiHelper.identify(properties, apiData);
    }
  }
  _exports.AnalyticsIdentify = AnalyticsIdentify;
  class AnalyticsGroup extends _baseMessage.default {
    /** @override */
    async execute(actionArgs, sender) {
      const {
        properties,
        apiData
      } = actionArgs;
      return segmentApiHelper.group(properties, apiData);
    }
  }
  _exports.AnalyticsGroup = AnalyticsGroup;
  class AnalyticsTrack extends _baseMessage.default {
    /** @override */
    async execute(actionArgs, sender) {
      const {
        eventName,
        properties,
        context,
        apiData
      } = actionArgs;
      return segmentApiHelper.trackEvent(eventName, properties, context, apiData);
    }
  }
  _exports.AnalyticsTrack = AnalyticsTrack;
});
Readable app analytics service from bundled source mapassets/chrome-ext.map:chrome-ext/services/app-analytics.js
define("chrome-ext/services/app-analytics", ["exports", "@glimmer/tracking", "@ember/service", "core/utils/object", "core/services/app-analytics", "core/constants/app-analytics"], function (_exports, _tracking, _service, _object, _appAnalytics, _appAnalytics2) {
  "use strict";

  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.default = void 0;
  0; //eaimeta@70e063a35619d71f0,"@glimmer/tracking",0,"@ember/service",0,"core/utils/object",0,"core/services/app-analytics",0,"core/constants/app-analytics"eaimeta@70e063a35619d71f
  class ChromeExtAppAnalyticsService extends _appAnalytics.default {
    static {
      dt7948.g(this.prototype, "appContext", [_service.inject]);
    }
    #appContext = (dt7948.i(this, "appContext"), void 0);
    static {
      dt7948.g(this.prototype, "webExtensionTunnel", [_service.inject]);
    }
    #webExtensionTunnel = (dt7948.i(this, "webExtensionTunnel"), void 0);
    static {
      dt7948.g(this.prototype, "hostIntegration", [_service.inject]);
    }
    #hostIntegration = (dt7948.i(this, "hostIntegration"), void 0);
    // -- Lifecycle Hooks ----------------------------------------------------------------------------

    constructor(...args) {
      super(...args);
      this.eventProperties = {
        client: 'chrome_extension',
        host: this.hostIntegration.getHost()
      };
      this.#initCrXVersion();
    }

    // -- Service Properties -------------------------------------------------------------------------

    /**
     * Used to make calls to Segment's API
     * @type {Object}
     * @type {String} Object.segmentWriteKey
     * @type {String} Object.companyUserId
     */
    segmentApiData = null;

    /** @type {String} */
    static {
      dt7948.g(this.prototype, "currentChromeExtensionVersion", [_tracking.tracked], function () {
        return null;
      });
    }
    #currentChromeExtensionVersion = (dt7948.i(this, "currentChromeExtensionVersion"), void 0);
    // -- Public Functions ---------------------------------------------------------------------------

    /** @type {Object} */
    get segmentAnalyticsContextData() {
      return {
        userAgent: window.navigator.userAgent,
        app: {
          version: this.currentChromeExtensionVersion,
          name: 'chrome_extension'
        }
      };
    }

    /**
     * Adds properties to the user to be identified by analytics libraries.
     *
     * @override
     * @param {Object} properties
     */
    addUserProperties(properties) {
      this.#addUserPropertiesSegment(properties);
    }

    /**
     * Adds properties for the group to be identified by analytics services.
     * For Copper, the group is the company associated with the currently active company user id.
     *
     * @override
     * @param {Object} properties group properties to send to analytics services
     */
    addUserGroupProperties(properties) {
      this.#addUserGroupPropertiesSegment(properties);
    }

    /**
     * Tells analytics libraries that the user is signed out.
     *
     * @override
     */
    signOutUser() {
      this.segmentApiData = null;
    }

    // -- Private Functions --------------------------------------------------------------------------

    /**
     * @typedef {Object} ViewInfo
     * @property {String} routeName - The name of the current route
     * @property {String} mainSection - The main section of the page. For entity pages, it is
     *                                  always 'entity', for rest of the app, it is the first
     *                                  section of the route.
     * @property {String} subSection - The sub section of the page. For entity pages, it is the
     *                                 type of the view (detail, list, pipeline, etc). For the
     *                                 rest of the app, it is the second section of the route.
     * @property {String} sourceEntity - The source entity of the page
     */

    /**
     * Using the active route, this function computes and returns the ViewInfo object defined above.
     *
     * @override
     * @param {RouteInfo} routeInfo An object containing data about the current route
     * @return {ViewInfo|Object}
     * @private
     */
    _getViewInfo(routeInfo) {
      if (routeInfo && routeInfo.name) {
        const routeName = routeInfo.name;
        const pathParams = routeInfo.params && this.#getFilteredPathParams(routeInfo.params);
        const routeSegments = routeName.split('.');
        if (routeSegments.length) {
          const baseViewInfo = {
            ...pathParams,
            routeName
          };
          const mainSection = 'entity';
          if (routeSegments[0] === 'profile') {
            // crx detail view
            return {
              ...baseViewInfo,
              mainSection,
              subSection: _appAnalytics2.ENTITY_SUB_SECTIONS.DETAIL,
              sourceEntity: _appAnalytics2.SINGULAR_ENTITY_NAME_TO_REPORTING_NAME_MAP[routeSegments[1]]
            };
          } else if (routeSegments[0] === 'lists') {
            // crx list view
            return {
              ...baseViewInfo,
              mainSection,
              subSection: _appAnalytics2.ENTITY_SUB_SECTIONS.LIST,
              sourceEntity: _appAnalytics2.PLURAL_ENTITY_NAME_TO_REPORTING_NAME_MAP[routeSegments[1]]
            };
          } else {
            return {
              ...this._getSections(routeSegments),
              ...baseViewInfo
            };
          }
        }
      }
      return {};
    }

    /**
     * Filters out any :filters path params from the provided params object for the purpose
     * of maintaining confidentiality with PII.
     * @param {Object} pathParams The path params (Required).
     * @returns {Object} The filtered params.
     */
    #getFilteredPathParams(pathParams) {
      // eslint-disable-next-line no-unused-vars
      const {
        filters,
        ...filteredPathParams
      } = pathParams;
      return filteredPathParams;
    }
    trackPageView(url) {
      this._trackAppEvent(_appAnalytics2.TRACKING_EVENT_NAMES.PAGE_VIEW, {
        properties: {
          url
        }
      });
    }

    /**
     * Sends tracked event logs to background js for further processing.
     *
     * @override
     * @param eventName
     * @param properties
     * @private
     */
    _logTrackedEvent(eventName, properties) {
      this.#logTrackedEventSegment(eventName, properties);
    }
    #logTrackedEventSegment(eventName, properties) {
      const apiData = this.segmentApiData;
      if (!apiData) {
        // We currently don't have enough Segment quota to track anonymous users,
        // so we don't provide the API key until they log in. Events may still
        // fire e.g. from the login page itself, and we have no way to get them to
        // Segment so we short-circuit them here.
        return;
      }
      const segmentParsedProperties = this.#stringifyRequiredValuesSegment((0, _object.decamelizeKeys)(properties));
      const context = this.segmentAnalyticsContextData;
      this.webExtensionTunnel.sendMessage({
        action: 'analytics.logTrackedEvent',
        eventName,
        properties: segmentParsedProperties,
        apiData,
        context
      });
    }
    #addUserPropertiesSegment(properties) {
      const parsedAnalyticsProperties = this.#stringifyRequiredValuesSegment((0, _object.decamelizeKeys)(properties));
      const analyticsPayload = {
        traits: parsedAnalyticsProperties,
        context: this.segmentAnalyticsContextData
      };
      this.webExtensionTunnel.sendMessage({
        action: 'analytics.addUserProperties',
        properties: analyticsPayload,
        apiData: this.segmentApiData
      });
    }

    /**
     * Sends a Segment group request from Ember to the Web Extension Tunnel
     * @param {Object} properties data to send to Segment
     */
    #addUserGroupPropertiesSegment(properties) {
      const decamelizedProperties = this.#stringifyRequiredValuesSegment((0, _object.decamelizeKeys)(properties));
      const analyticsPayload = {
        traits: decamelizedProperties,
        context: this.segmentAnalyticsContextData
      };
      this.webExtensionTunnel.sendMessage({
        action: 'analytics.addGroupProperties',
        properties: analyticsPayload,
        apiData: this.segmentApiData
      });
    }

    /**
     * Returns an object with values converted to Strings per Segment specification
     * @private
     * @param {Object} values traits sent to Segment
     * @return {Object}
     */
    #stringifyRequiredValuesSegment(values) {
      const REQUIRED_STRING_FIELDS = ['internal_company_id', 'internal_company_user_id', 'internal_user_id'];
      const valuesClone = {
        ...values
      };
      REQUIRED_STRING_FIELDS.forEach(requiredStringFieldKey => {
        if (valuesClone.hasOwnProperty(requiredStringFieldKey)) {
          valuesClone[requiredStringFieldKey] = String(valuesClone[requiredStringFieldKey]);
        }
      });
      return valuesClone;
    }
    async #initCrXVersion() {
      this.currentChromeExtensionVersion = await this.webExtensionTunnel.sendMessage({
        action: 'runtime.manifestVersion'
      });
    }
  }
  _exports.default = ChromeExtAppAnalyticsService;
});
Readable app context from bundled source mapassets/chrome-ext.map:chrome-ext/services/app-context.js
define("chrome-ext/services/app-context", ["exports", "core/services/app-context", "@ember/service"], function (_exports, _appContext, _service) {
  "use strict";

  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.default = void 0;
  0; //eaimeta@70e063a35619d71f0,"core/services/app-context",0,"@ember/service"eaimeta@70e063a35619d71f
  var _default = _exports.default = _appContext.default.extend({
    hostIntegration: (0, _service.inject)(),
    appAnalytics: (0, _service.inject)(),
    update(jsonData, ignoreCompanyUser) {
      this._super(jsonData, ignoreCompanyUser);
      const segmentRequestRequiredData = {
        segmentWriteKey: jsonData.segment.chromeExtensionApiKey,
        companyUserId: this.companyUser.id
      };
      this.hostIntegration.api.Config.enableContentScriptEventTracking((eventName, properties, options) => {
        this.appAnalytics.trackCustomEvent(eventName, {
          properties,
          options
        });
      });
      this.appAnalytics.segmentApiData = segmentRequestRequiredData;
    }
  });
});
05EvidenceTHIRD PARTY LIST
External analytics destination
  • api.segment.io

    Segment HTTP API endpoint receiving track, identify, and group analytics requests from the extension background worker.

Updated 30 September 2026hpfmedbkgaakgagknibnonpkimkibkla