Skip to content

Commit 03712f0

Browse files
committed
fix(packages): Prevent prototype pollution by Validating dynamic keys across controllers
1 parent 567e202 commit 03712f0

File tree

7 files changed

+67
-9
lines changed

7 files changed

+67
-9
lines changed

packages/assets-controllers/src/AccountTrackerController.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,14 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
704704
) {
705705
this.update((state) => {
706706
balances.forEach(({ address, chainId, balance }) => {
707+
// Prevent prototype pollution with dangerous keys
708+
if (
709+
chainId === '__proto__' ||
710+
chainId === 'constructor' ||
711+
chainId === 'prototype'
712+
) {
713+
return;
714+
}
707715
const checksumAddress = toChecksumHexAddress(address);
708716

709717
// Ensure the chainId exists in the state
@@ -740,6 +748,14 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
740748
) {
741749
this.update((state) => {
742750
stakedBalances.forEach(({ address, chainId, stakedBalance }) => {
751+
// Prevent prototype pollution with dangerous keys
752+
if (
753+
chainId === '__proto__' ||
754+
chainId === 'constructor' ||
755+
chainId === 'prototype'
756+
) {
757+
return;
758+
}
743759
const checksumAddress = toChecksumHexAddress(address);
744760

745761
// Ensure the chainId exists in the state

packages/assets-controllers/src/TokenBalancesController.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,14 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
594594
// First, initialize all tokens from allTokens state with balance 0
595595
// for the accounts and chains we're processing
596596
for (const chainId of targetChains) {
597+
// Prevent prototype pollution by skipping dangerous property names
598+
if (
599+
chainId === '__proto__' ||
600+
chainId === 'constructor' ||
601+
chainId === 'prototype'
602+
) {
603+
continue;
604+
}
597605
for (const account of accountsToProcess) {
598606
// Initialize tokens from allTokens
599607
const chainTokens = this.#allTokens[chainId];
@@ -625,6 +633,14 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
625633

626634
// Then update with actual fetched balances where available
627635
aggregated.forEach(({ success, value, account, token, chainId }) => {
636+
// Prevent prototype pollution by skipping dangerous property names
637+
if (
638+
chainId === '__proto__' ||
639+
chainId === 'constructor' ||
640+
chainId === 'prototype'
641+
) {
642+
return;
643+
}
628644
if (success && value !== undefined) {
629645
((d.tokenBalances[account] ??= {})[chainId] ??= {})[checksum(token)] =
630646
toHex(value);

packages/earn-controller/src/selectors.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ export const selectLendingMarketsByProtocolAndId = createSelector(
2323
(markets) => {
2424
return markets.reduce(
2525
(acc, market) => {
26-
acc[market.protocol] = acc[market.protocol] || {};
27-
acc[market.protocol][market.id] = market;
26+
if (!acc.has(market.protocol)) {
27+
acc.set(market.protocol, new Map());
28+
}
29+
acc.get(market.protocol)!.set(market.id, market);
2830
return acc;
2931
},
30-
{} as Record<string, Record<string, LendingMarket>>,
32+
new Map<string, Map<string, LendingMarket>>(),
3133
);
3234
},
3335
);
@@ -38,7 +40,9 @@ export const selectLendingMarketForProtocolAndId = (
3840
) =>
3941
createSelector(
4042
selectLendingMarketsByProtocolAndId,
41-
(marketsByProtocolAndId) => marketsByProtocolAndId?.[protocol]?.[id],
43+
(marketsByProtocolAndId) => {
44+
return marketsByProtocolAndId?.get(protocol)?.get(id);
45+
},
4246
);
4347

4448
export const selectLendingMarketsByChainId = createSelector(
@@ -63,7 +67,7 @@ export const selectLendingPositionsWithMarket = createSelector(
6367
return {
6468
...position,
6569
market:
66-
marketsByProtocolAndId?.[position.protocol]?.[position.marketId],
70+
marketsByProtocolAndId?.get(position.protocol)?.get(position.marketId),
6771
};
6872
});
6973
},

packages/ens-controller/src/EnsController.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,14 @@ export class EnsController extends BaseController<
196196
*/
197197
delete(chainId: Hex, ensName: string): boolean {
198198
const normalizedEnsName = normalizeEnsName(ensName);
199+
// Defense-in-depth: block prototype-polluting property names.
200+
const unsafeKeys = ['__proto__', 'prototype', 'constructor'];
199201
if (
200202
!isSafeDynamicKey(chainId) ||
201203
!normalizedEnsName ||
202204
!this.state.ensEntries[chainId] ||
203-
!this.state.ensEntries[chainId][normalizedEnsName]
205+
!this.state.ensEntries[chainId][normalizedEnsName] ||
206+
unsafeKeys.includes(chainId)
204207
) {
205208
return false;
206209
}

packages/name-controller/src/NameController.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,14 +467,14 @@ export class NameController extends BaseController<
467467
}
468468

469469
this.update((state) => {
470-
const typeEntries = state.names[type] || {};
470+
const typeEntries = state.names[type] || Object.create(null);
471471
state.names[type] = typeEntries;
472472

473-
const variationEntries = typeEntries[normalizedValue] || {};
473+
const variationEntries = typeEntries[normalizedValue] || Object.create(null);
474474
typeEntries[normalizedValue] = variationEntries;
475475

476476
const entry = variationEntries[normalizedVariation] ?? {
477-
proposedNames: {},
477+
proposedNames: Object.create(null),
478478
name: null,
479479
sourceId: null,
480480
origin: null,

packages/network-enablement-controller/src/NetworkEnablementController.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,17 @@ export class NetworkEnablementController extends BaseController<
252252
);
253253
}
254254

255+
// Prevent prototype pollution via dangerous property names
256+
if (
257+
namespace === '__proto__' ||
258+
namespace === 'constructor' ||
259+
namespace === 'prototype'
260+
) {
261+
throw new Error(
262+
`Invalid namespace: "${namespace}" is not allowed.`,
263+
);
264+
}
265+
255266
this.update((s) => {
256267
// Ensure the namespace bucket exists
257268
this.#ensureNamespaceBucket(s, namespace);

packages/sample-controllers/src/sample-petnames-controller.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,14 @@ export class SamplePetnamesController extends BaseController<
211211
if (!isSafeDynamicKey(chainId)) {
212212
throw new Error('Invalid chain ID');
213213
}
214+
// Explicitly forbid keys that may cause prototype pollution.
215+
if (
216+
chainId === '__proto__' ||
217+
chainId === 'constructor' ||
218+
chainId === 'prototype'
219+
) {
220+
throw new Error('Unsafe chain ID');
221+
}
214222

215223
const normalizedAddress = address.toLowerCase() as Hex;
216224

0 commit comments

Comments
 (0)