Skip to content

Commit 586842f

Browse files
feat: sanitize non-EVM account addresses from Sentry events (#43337)
## **Description** Extend Sentry address sanitization beyond EVM addresses to cover Solana, Tron, Stellar, and Bitcoin (bech32 + legacy) formats. Also, sanitization now also runs over breadcrumb message/data and report `extra`/`contexts`, with a recursive walk that handles cyclic references and non-enumerable `Error` `message`/`stack` fields. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: MetaMask/MetaMask-planning#6680 ## **Manual testing steps** 1. Update Extension code base to artificially include evm and non-evm addresses in the different fields of `Capture UI Error`, like done on branch [feat/sanitize-non-evm-addresses-sentry-test](#43338) 2. Update `.metamaskrc` a. Set SENTRY_DSN_DEV=_dsn_of_your_personal_sentry_account_ b. Set ENABLE_SETTINGS_PAGE_DEV_OPTIONS=true 3. Build Extension with `yarn start --sentry` 4. Go through Extension onboarding and enable metametrics 5. Go to Settings > Debug and click `Capture UI Error` 6. Go to Sentry and confirm the error got captured but all evm and non-evm addresses are sanitized <img width="374" height="550" alt="Screenshot 2026-06-08 at 17 14 24" src="https://github.com/user-attachments/assets/18d27649-2a90-41b0-8040-1c23d0cd1209" /> ## **Screenshots/Recordings** ### Error message <img width="826" height="127" alt="Screenshot 2026-06-08 at 18 03 14" src="https://github.com/user-attachments/assets/2170d929-944f-41ab-b3a7-f7082acfa2f4" /> ### Stack trace <img width="782" height="137" alt="Screenshot 2026-06-08 at 18 03 23" src="https://github.com/user-attachments/assets/64465b98-d0d2-4c8b-a11b-6f0ce3d10002" /> ### Breadcrumbs <img width="786" height="864" alt="Screenshot 2026-06-08 at 18 04 40" src="https://github.com/user-attachments/assets/c828f9d1-48a6-44e8-850b-0706f1d31611" /> ### Additional data (extra) <img width="777" height="606" alt="Screenshot 2026-06-08 at 17 28 08" src="https://github.com/user-attachments/assets/6de88058-22af-47c1-85a9-f6d36c4d2274" /> ### Context <img width="778" height="651" alt="Screenshot 2026-06-08 at 19 27 16" src="https://github.com/user-attachments/assets/bbb3ecc2-9f8b-433d-80dd-c7b4ea5361d6" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Telemetry-only privacy hardening in `setupSentry`; broad base58 patterns may over-redact some strings but do not affect wallet or auth flows. > > **Overview** > Expands **Sentry privacy scrubbing** so wallet addresses are stripped from more payload shapes and chain formats, not only EVM hex in error text. > > **Address formats:** Error-message sanitization now routes through `sanitizeAddressesFromString`, which masks EVM as `0x**` and adds regexes for Tron, Stellar, Bitcoin (bech32 and legacy), and Solana (EVM is applied first so masks are not re-matched). > > **Broader coverage:** `rewriteReport` runs new `sanitizeAddressesFromReportData` on `extra` and `contexts` before app state is merged in. Breadcrumbs get the same treatment on `message` and `data` via `sanitizeAddressesFromObject`, which deep-copies strings (handles cycles, shared arrays, and `Error` `message`/`stack`) so live console `arguments` are not mutated. > > Tests cover each chain in messages, nested `extra`/`contexts`, shared references, and non-mutating breadcrumb redaction. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9c1292a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b7cc8d0 commit 586842f

2 files changed

Lines changed: 288 additions & 4 deletions

File tree

app/scripts/lib/setupSentry.js

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,14 @@ export function removeUrlsFromBreadCrumb(breadcrumb) {
353353
if (breadcrumb?.data?.from) {
354354
breadcrumb.data.from = hideUrlIfNotInternal(breadcrumb.data.from);
355355
}
356+
// Sanitize any account addresses that may appear in the breadcrumb message or
357+
// remaining data values.
358+
if (typeof breadcrumb?.message === 'string') {
359+
breadcrumb.message = sanitizeAddressesFromString(breadcrumb.message);
360+
}
361+
if (breadcrumb?.data) {
362+
breadcrumb.data = sanitizeAddressesFromObject(breadcrumb.data);
363+
}
356364
return breadcrumb;
357365
}
358366

@@ -374,6 +382,10 @@ export function rewriteReport(report) {
374382
// but putting the code here as well gives public visibility to how we are handling
375383
// privacy with respect to sentry.
376384
sanitizeAddressesFromErrorMessages(report);
385+
// Remove addresses from other error parameters (extra, contexts).
386+
// Done before attaching appState below so the (already masked) appState is
387+
// not re-walked.
388+
sanitizeAddressesFromReportData(report);
377389
// modify report urls
378390
rewriteReportUrls(report);
379391

@@ -442,10 +454,112 @@ function sanitizeUrlsFromErrorMessages(report) {
442454
* @param {object} report - the report to modify
443455
*/
444456
function sanitizeAddressesFromErrorMessages(report) {
445-
rewriteErrorMessages(report, (errorMessage) => {
446-
const newErrorMessage = errorMessage.replace(/0x[A-Fa-f0-9]{40}/u, '0x**');
447-
return newErrorMessage;
448-
});
457+
rewriteErrorMessages(report, (errorMessage) =>
458+
sanitizeAddressesFromString(errorMessage),
459+
);
460+
}
461+
462+
// Patterns for sanitizing account addresses before sending events to Sentry.
463+
// EVM is handled separately so it can keep its `0x**` replacement form.
464+
const EVM_ADDRESS_REGEX = /0x[A-Fa-f0-9]{40}/gu;
465+
const NON_EVM_ADDRESS_REGEXES = [
466+
// Tron (base58, starts with `T`, 34 chars total)
467+
/\bT[1-9A-HJ-NP-Za-km-z]{33}\b/gu,
468+
// Stellar / XLM (starts with `G`, 56 chars total)
469+
/\bG[A-Z2-7]{55}\b/gu,
470+
// Bitcoin bech32 / taproot (`bc1...`)
471+
/\bbc1[02-9ac-hj-np-z]{6,87}\b/gu,
472+
// Bitcoin legacy P2PKH / P2SH (base58, starts with `1` or `3`)
473+
/\b[13][1-9A-HJ-NP-Za-km-z]{25,34}\b/gu,
474+
// Solana (base58, 32-44 chars). Kept last as its range overlaps the others.
475+
/\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/gu,
476+
];
477+
478+
/**
479+
* Sanitizes EVM and non-EVM account addresses from a string.
480+
*
481+
* @param {string} text - The string to sanitize addresses from.
482+
* @returns {string} The string with any addresses replaced by a mask.
483+
*/
484+
function sanitizeAddressesFromString(text) {
485+
// Sanitize EVM addresses first so the resulting `0x**` cannot be re-matched by
486+
// the base58 patterns below.
487+
let sanitized = text.replace(EVM_ADDRESS_REGEX, '0x**');
488+
for (const regex of NON_EVM_ADDRESS_REGEXES) {
489+
sanitized = sanitized.replace(regex, '**');
490+
}
491+
return sanitized;
492+
}
493+
494+
/**
495+
* Recursively sanitizes account addresses from the string values of an object,
496+
* returning a sanitized copy without mutating the input. Used to scrub addresses
497+
* that may appear in error parameters such as `report.extra`/`report.contexts`
498+
* and in breadcrumb data. Not mutating matters for breadcrumbs, whose
499+
* `data.arguments` holds live references (e.g. the thrown `Error`) that the
500+
* extension may still use after the event is sent.
501+
*
502+
* @param {*} value - The value to sanitize addresses from.
503+
* @param {WeakMap} [seen] - Maps already-visited inputs to their sanitized copy,
504+
* so shared references stay consistent and cyclic structures terminate.
505+
* @returns {*} The sanitized value (a copy for objects/arrays).
506+
*/
507+
function sanitizeAddressesFromObject(value, seen = new WeakMap()) {
508+
if (typeof value === 'string') {
509+
return sanitizeAddressesFromString(value);
510+
}
511+
// Leave primitives (and null) untouched.
512+
if (value === null || typeof value !== 'object') {
513+
return value;
514+
}
515+
// Reuse the sanitized copy for any reference we've already processed, so shared
516+
// references stay consistent and cyclic structures don't loop forever.
517+
if (seen.has(value)) {
518+
return seen.get(value);
519+
}
520+
521+
if (Array.isArray(value)) {
522+
const copy = [];
523+
seen.set(value, copy);
524+
for (let i = 0; i < value.length; i++) {
525+
copy[i] = sanitizeAddressesFromObject(value[i], seen);
526+
}
527+
return copy;
528+
}
529+
530+
const copy = {};
531+
seen.set(value, copy);
532+
// `Error` carries its address-bearing data on `message`/`stack`, which are
533+
// non-enumerable and so invisible to the `Object.keys` walk below. These show
534+
// up e.g. in console breadcrumbs, whose `data.arguments` holds the raw thrown
535+
// error. Copy them across explicitly, sanitized.
536+
if (value instanceof Error) {
537+
if (typeof value.message === 'string') {
538+
copy.message = sanitizeAddressesFromString(value.message);
539+
}
540+
if (typeof value.stack === 'string') {
541+
copy.stack = sanitizeAddressesFromString(value.stack);
542+
}
543+
}
544+
for (const key of Object.keys(value)) {
545+
copy[key] = sanitizeAddressesFromObject(value[key], seen);
546+
}
547+
return copy;
548+
}
549+
550+
/**
551+
* Receives a Sentry event object and sanitizes account addresses from its
552+
* error parameters (`extra` and `contexts`).
553+
*
554+
* @param {object} report - the report to modify
555+
*/
556+
function sanitizeAddressesFromReportData(report) {
557+
if (report.extra) {
558+
report.extra = sanitizeAddressesFromObject(report.extra);
559+
}
560+
if (report.contexts) {
561+
report.contexts = sanitizeAddressesFromObject(report.contexts);
562+
}
449563
}
450564

451565
function simplifyErrorMessages(report) {

app/scripts/lib/setupSentry.test.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,123 @@ describe('Setup Sentry', () => {
157157
rewriteReport(testReport);
158158
expect(testReport.message).toStrictEqual('This is a simple report');
159159
});
160+
161+
it('removes Solana addresses from error messages', () => {
162+
const testReport = {
163+
message:
164+
'There is a Solana address 7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs in this message',
165+
request: {},
166+
};
167+
rewriteReport(testReport);
168+
expect(testReport.message).toStrictEqual(
169+
'There is a Solana address ** in this message',
170+
);
171+
});
172+
173+
it('removes Tron addresses from error messages', () => {
174+
const testReport = {
175+
message:
176+
'There is a Tron address TJRyWwFs9wTFGZg3JbrVriFbNfCug5tDeC in this message',
177+
request: {},
178+
};
179+
rewriteReport(testReport);
180+
expect(testReport.message).toStrictEqual(
181+
'There is a Tron address ** in this message',
182+
);
183+
});
184+
185+
it('removes Stellar (XLM) addresses from error messages', () => {
186+
const testReport = {
187+
message:
188+
'There is a Stellar address GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX in this message',
189+
request: {},
190+
};
191+
rewriteReport(testReport);
192+
expect(testReport.message).toStrictEqual(
193+
'There is a Stellar address ** in this message',
194+
);
195+
});
196+
197+
it('removes Bitcoin bech32 addresses from error messages', () => {
198+
const testReport = {
199+
message:
200+
'There is a Bitcoin address bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq in this message',
201+
request: {},
202+
};
203+
rewriteReport(testReport);
204+
expect(testReport.message).toStrictEqual(
205+
'There is a Bitcoin address ** in this message',
206+
);
207+
});
208+
209+
it('removes Bitcoin legacy addresses from error messages', () => {
210+
const testReport = {
211+
message:
212+
'There is a Bitcoin address 17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem in this message',
213+
request: {},
214+
};
215+
rewriteReport(testReport);
216+
expect(testReport.message).toStrictEqual(
217+
'There is a Bitcoin address ** in this message',
218+
);
219+
});
220+
221+
it('removes multiple EVM addresses from a single error message', () => {
222+
const testReport = {
223+
message:
224+
'Addresses 0x790A8A9E9bc1C9dB991D8721a92e461Db4CfB235 and 0x1234567890123456789012345678901234567890 failed',
225+
request: {},
226+
};
227+
rewriteReport(testReport);
228+
expect(testReport.message).toStrictEqual(
229+
'Addresses 0x** and 0x** failed',
230+
);
231+
});
232+
233+
it('removes addresses from report.extra parameters', () => {
234+
const testReport = {
235+
message: 'An error occurred',
236+
extra: {
237+
accountAddress: '7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs',
238+
nested: {
239+
evmAddress: '0x790A8A9E9bc1C9dB991D8721a92e461Db4CfB235',
240+
},
241+
},
242+
request: {},
243+
};
244+
rewriteReport(testReport);
245+
expect(testReport.extra.accountAddress).toStrictEqual('**');
246+
expect(testReport.extra.nested.evmAddress).toStrictEqual('0x**');
247+
});
248+
249+
it('removes addresses from an array shared across multiple properties', () => {
250+
const sharedAddresses = ['7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs'];
251+
const testReport = {
252+
message: 'An error occurred',
253+
extra: {
254+
first: sharedAddresses,
255+
second: sharedAddresses,
256+
},
257+
request: {},
258+
};
259+
rewriteReport(testReport);
260+
expect(testReport.extra.first).toStrictEqual(['**']);
261+
expect(testReport.extra.second).toStrictEqual(['**']);
262+
});
263+
264+
it('removes addresses from report.contexts parameters', () => {
265+
const testReport = {
266+
message: 'An error occurred',
267+
contexts: {
268+
account: {
269+
address: 'TJRyWwFs9wTFGZg3JbrVriFbNfCug5tDeC',
270+
},
271+
},
272+
request: {},
273+
};
274+
rewriteReport(testReport);
275+
expect(testReport.contexts.account.address).toStrictEqual('**');
276+
});
160277
});
161278

162279
describe('shouldCreateSpanForRequest', () => {
@@ -350,5 +467,58 @@ describe('Setup Sentry', () => {
350467
from: 'chrome-extension://abcefg/home.html',
351468
});
352469
});
470+
471+
it('removes addresses from the breadcrumb message', () => {
472+
const testBreadcrumb = {
473+
message:
474+
'Selected account 7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs',
475+
data: {
476+
address: '0x790A8A9E9bc1C9dB991D8721a92e461Db4CfB235',
477+
},
478+
};
479+
const rewrittenBreadcrumb = removeUrlsFromBreadCrumb(testBreadcrumb);
480+
expect(rewrittenBreadcrumb.message).toStrictEqual('Selected account **');
481+
});
482+
483+
it('removes addresses from the breadcrumb data', () => {
484+
const testBreadcrumb = {
485+
message:
486+
'Selected account 7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs',
487+
data: {
488+
address: '0x790A8A9E9bc1C9dB991D8721a92e461Db4CfB235',
489+
},
490+
};
491+
const rewrittenBreadcrumb = removeUrlsFromBreadCrumb(testBreadcrumb);
492+
expect(rewrittenBreadcrumb.data.address).toStrictEqual('0x**');
493+
});
494+
495+
it('redacts the breadcrumb data without mutating the live source objects', () => {
496+
const liveError = new Error(
497+
'Failed for 0x790A8A9E9bc1C9dB991D8721a92e461Db4CfB235',
498+
);
499+
const liveArgs = [
500+
liveError,
501+
'7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs',
502+
];
503+
const testBreadcrumb = {
504+
message: 'console.error',
505+
data: { arguments: liveArgs, logger: 'console' },
506+
};
507+
508+
const rewrittenBreadcrumb = removeUrlsFromBreadCrumb(testBreadcrumb);
509+
510+
// The breadcrumb sent to Sentry is redacted...
511+
expect(rewrittenBreadcrumb.data.arguments[0].message).toStrictEqual(
512+
'Failed for 0x**',
513+
);
514+
expect(rewrittenBreadcrumb.data.arguments[1]).toStrictEqual('**');
515+
// ...but the live Error and array the extension still holds are untouched.
516+
expect(liveError.message).toStrictEqual(
517+
'Failed for 0x790A8A9E9bc1C9dB991D8721a92e461Db4CfB235',
518+
);
519+
expect(liveArgs[1]).toStrictEqual(
520+
'7EYnhQoR9YM3N7UoaKRoA44Uy8JeaZV3qyouov87awMs',
521+
);
522+
});
353523
});
354524
});

0 commit comments

Comments
 (0)