1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 19:26:13 +02:00
metamask-extension/app/scripts/lib/setupSentry.js
Dan J Miller ba3914e9fe
Sanitize privacy sensitive data before sending to sentry. (#16780)
* Sanitize privacy sensitive data before sending to sentry.

Temp

Near complete

Complete

* Temp

* Fix url error message rewrite

* Add unit tests and cleanup code

* Improvements

* Update app/scripts/lib/setupSentry.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Update app/scripts/lib/setupSentry.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Update app/scripts/lib/setupSentry.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Fix syntax of doc comments

* Catch errors caused by invalid urls in sanitizeUrlsFromErrorMessages

* Ensure our allowlist matches multiple subdomains

* Ensure sanitizeUrlsFromErrorMessages correctly matches hostnames

* Update app/scripts/lib/setupSentry.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Improve test descriptions

* fix

Co-authored-by: Ariella Vu <20778143+digiwand@users.noreply.github.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
2022-12-16 11:56:23 -03:30

340 lines
10 KiB
JavaScript

import * as Sentry from '@sentry/browser';
import { Dedupe, ExtraErrorData } from '@sentry/integrations';
import { BuildType } from '../../../shared/constants/app';
import { FilterEvents } from './sentry-filter-events';
import extractEthjsErrorMessage from './extractEthjsErrorMessage';
/* eslint-disable prefer-destructuring */
// Destructuring breaks the inlining of the environment variables
const METAMASK_DEBUG = process.env.METAMASK_DEBUG;
const METAMASK_ENVIRONMENT = process.env.METAMASK_ENVIRONMENT;
const SENTRY_DSN_DEV =
process.env.SENTRY_DSN_DEV ||
'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496';
const METAMASK_BUILD_TYPE = process.env.METAMASK_BUILD_TYPE;
const IN_TEST = process.env.IN_TEST;
/* eslint-enable prefer-destructuring */
export const ERROR_URL_ALLOWLIST = {
CRYPTOCOMPARE: 'cryptocompare.com',
COINGECKO: 'coingecko.com',
ETHERSCAN: 'etherscan.io',
CODEFI: 'codefi.network',
SEGMENT: 'segment.io',
};
// This describes the subset of Redux state attached to errors sent to Sentry
// These properties have some potential to be useful for debugging, and they do
// not contain any identifiable information.
export const SENTRY_STATE = {
gas: true,
history: true,
metamask: {
alertEnabledness: true,
completedOnboarding: true,
connectedStatusPopoverHasBeenShown: true,
conversionDate: true,
conversionRate: true,
currentBlockGasLimit: true,
currentCurrency: true,
currentLocale: true,
customNonceValue: true,
defaultHomeActiveTabName: true,
featureFlags: true,
firstTimeFlowType: true,
forgottenPassword: true,
incomingTxLastFetchedBlockByChainId: true,
ipfsGateway: true,
isAccountMenuOpen: true,
isInitialized: true,
isUnlocked: true,
metaMetricsId: true,
nativeCurrency: true,
network: true,
nextNonce: true,
participateInMetaMetrics: true,
preferences: true,
provider: {
nickname: true,
ticker: true,
type: true,
},
seedPhraseBackedUp: true,
unapprovedDecryptMsgCount: true,
unapprovedEncryptionPublicKeyMsgCount: true,
unapprovedMsgCount: true,
unapprovedPersonalMsgCount: true,
unapprovedTypedMessagesCount: true,
useBlockie: true,
useNonceField: true,
usePhishDetect: true,
welcomeScreenSeen: true,
},
unconnectedAccount: true,
};
export default function setupSentry({ release, getState }) {
if (!release) {
throw new Error('Missing release');
} else if (METAMASK_DEBUG && !IN_TEST) {
/**
* Workaround until the following issue is resolved
* https://github.com/MetaMask/metamask-extension/issues/15691
* The IN_TEST condition allows the e2e tests to run with both
* yarn start:test and yarn build:test
*/
return undefined;
}
const environment =
METAMASK_BUILD_TYPE === BuildType.main
? METAMASK_ENVIRONMENT
: `${METAMASK_ENVIRONMENT}-${METAMASK_BUILD_TYPE}`;
let sentryTarget;
if (METAMASK_ENVIRONMENT === 'production') {
if (!process.env.SENTRY_DSN) {
throw new Error(
`Missing SENTRY_DSN environment variable in production environment`,
);
}
console.log(
`Setting up Sentry Remote Error Reporting for '${environment}': SENTRY_DSN`,
);
sentryTarget = process.env.SENTRY_DSN;
} else {
console.log(
`Setting up Sentry Remote Error Reporting for '${environment}': SENTRY_DSN_DEV`,
);
sentryTarget = SENTRY_DSN_DEV;
}
/**
* A function that returns whether MetaMetrics is enabled. This should also
* return `false` if state has not yet been initialzed.
*
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
* is enabled, `false` otherwise.
*/
function getMetaMetricsEnabled() {
if (getState) {
const appState = getState();
if (!appState?.store?.metamask?.participateInMetaMetrics) {
return false;
}
} else {
return false;
}
return true;
}
Sentry.init({
dsn: sentryTarget,
debug: METAMASK_DEBUG,
environment,
integrations: [
new FilterEvents({ getMetaMetricsEnabled }),
new Dedupe(),
new ExtraErrorData(),
],
release,
beforeSend: (report) => rewriteReport(report, getState),
beforeBreadcrumb(breadcrumb) {
if (getState) {
const appState = getState();
if (
Object.values(appState).length &&
(!appState?.store?.metamask?.participateInMetaMetrics ||
!appState?.store?.metamask?.completedOnboarding ||
breadcrumb?.category === 'ui.input')
) {
return null;
}
} else {
return null;
}
const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb);
return newBreadcrumb;
},
});
return Sentry;
}
/**
* Receives a string and returns that string if it is a
* regex match for a url with a `chrome-extension` or `moz-extension`
* protocol, and an empty string otherwise.
*
* @param {string} url - The URL to check.
* @returns {string} An empty string if the URL was internal, or the unmodified URL otherwise.
*/
function hideUrlIfNotInternal(url) {
const re = /^(chrome-extension|moz-extension):\/\//u;
if (!url.match(re)) {
return '';
}
return url;
}
/**
* Receives a Sentry breadcrumb object and potentially removes urls
* from its `data` property, it particular those possibly found at
* data.from, data.to and data.url
*
* @param {object} breadcrumb - A Sentry breadcrumb object: https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/
* @returns {object} A modified Sentry breadcrumb object.
*/
export function removeUrlsFromBreadCrumb(breadcrumb) {
if (breadcrumb?.data?.url) {
breadcrumb.data.url = hideUrlIfNotInternal(breadcrumb.data.url);
}
if (breadcrumb?.data?.to) {
breadcrumb.data.to = hideUrlIfNotInternal(breadcrumb.data.to);
}
if (breadcrumb?.data?.from) {
breadcrumb.data.from = hideUrlIfNotInternal(breadcrumb.data.from);
}
return breadcrumb;
}
/**
* Receives a Sentry event object and modifies it before the
* error is sent to Sentry. Modifications include both sanitization
* of data via helper methods and addition of state data from the
* return value of the second parameter passed to the function.
*
* @param {object} report - A Sentry event object: https://develop.sentry.dev/sdk/event-payloads/
* @param {Function} getState - A function that should return an object representing some amount
* of app state that we wish to submit with our error reports
* @returns {object} A modified Sentry event object.
*/
export function rewriteReport(report, getState) {
try {
// simplify certain complex error messages (e.g. Ethjs)
simplifyErrorMessages(report);
// remove urls from error message
sanitizeUrlsFromErrorMessages(report);
// Remove evm addresses from error message.
// Note that this is redundent with data scrubbing we do within our sentry dashboard,
// but putting the code here as well gives public visibility to how we are handling
// privacy with respect to sentry.
sanitizeAddressesFromErrorMessages(report);
// modify report urls
rewriteReportUrls(report);
// append app state
if (getState) {
const appState = getState();
if (!report.extra) {
report.extra = {};
}
report.extra.appState = appState;
}
} catch (err) {
console.warn(err);
}
return report;
}
/**
* Receives a Sentry event object and modifies it so that urls are removed from any of its
* error messages.
*
* @param {object} report - the report to modify
*/
function sanitizeUrlsFromErrorMessages(report) {
rewriteErrorMessages(report, (errorMessage) => {
let newErrorMessage = errorMessage;
const re = /(([-.+a-zA-Z]+:\/\/)|(www\.))\S+[@:.]\S+/gu;
const urlsInMessage = newErrorMessage.match(re) || [];
urlsInMessage.forEach((url) => {
try {
const urlObj = new URL(url);
const { hostname } = urlObj;
if (
!Object.values(ERROR_URL_ALLOWLIST).some(
(allowedHostname) =>
hostname === allowedHostname ||
hostname.endsWith(`.${allowedHostname}`),
)
) {
newErrorMessage = newErrorMessage.replace(url, '**');
}
} catch (e) {
newErrorMessage = newErrorMessage.replace(url, '**');
}
});
return newErrorMessage;
});
}
/**
* Receives a Sentry event object and modifies it so that ethereum addresses are removed from
* any of its error messages.
*
* @param {object} report - the report to modify
*/
function sanitizeAddressesFromErrorMessages(report) {
rewriteErrorMessages(report, (errorMessage) => {
const newErrorMessage = errorMessage.replace(/0x[A-Fa-f0-9]{40}/u, '0x**');
return newErrorMessage;
});
}
function simplifyErrorMessages(report) {
rewriteErrorMessages(report, (errorMessage) => {
// simplify ethjs error messages
let simplifiedErrorMessage = extractEthjsErrorMessage(errorMessage);
// simplify 'Transaction Failed: known transaction'
if (
simplifiedErrorMessage.indexOf(
'Transaction Failed: known transaction',
) === 0
) {
// cut the hash from the error message
simplifiedErrorMessage = 'Transaction Failed: known transaction';
}
return simplifiedErrorMessage;
});
}
function rewriteErrorMessages(report, rewriteFn) {
// rewrite top level message
if (typeof report.message === 'string') {
report.message = rewriteFn(report.message);
}
// rewrite each exception message
if (report.exception && report.exception.values) {
report.exception.values.forEach((item) => {
if (typeof item.value === 'string') {
item.value = rewriteFn(item.value);
}
});
}
}
function rewriteReportUrls(report) {
// update request url
report.request.url = toMetamaskUrl(report.request.url);
// update exception stack trace
if (report.exception && report.exception.values) {
report.exception.values.forEach((item) => {
if (item.stacktrace) {
item.stacktrace.frames.forEach((frame) => {
frame.filename = toMetamaskUrl(frame.filename);
});
}
});
}
}
function toMetamaskUrl(origUrl) {
const filePath = origUrl?.split(globalThis.location.origin)[1];
if (!filePath) {
return origUrl;
}
const metamaskUrl = `metamask${filePath}`;
return metamaskUrl;
}