Skip to content

Commit 9d1c82a

Browse files
lucasgabrielgspjagdeep sidhu
authored andcommitted
[Vulnerability 005] - fix: solve Cached ENS Lookup in UI
1 parent 0f7273b commit 9d1c82a

File tree

6 files changed

+101
-21
lines changed

6 files changed

+101
-21
lines changed

source/pages/Home/Panel/components/Transactions/EVM/EvmDetailsEnhanced.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { useTransactionsListConfig, useUtils } from 'hooks/index';
1414
import { useController } from 'hooks/useController';
1515
import type { IEvmTransactionResponse } from 'scripts/Background/controllers/transactions/types';
1616
import { RootState } from 'state/store';
17-
import { selectActiveAccount } from 'state/vault/selectors';
17+
import {
18+
selectActiveAccount,
19+
selectValidEnsCache,
20+
} from 'state/vault/selectors';
1821
import { IDecodedTx } from 'types/transactions';
1922
import { formatMethodName } from 'utils/commonMethodSignatures';
2023
import { camelCaseToText } from 'utils/index';
@@ -37,9 +40,8 @@ export const EvmTransactionDetailsEnhanced = ({
3740
tx: IEvmTransactionResponse;
3841
}) => {
3942
const { controllerEmitter } = useController();
40-
const ensCache = useSelector(
41-
(state: RootState) => state.vaultGlobal.ensCache
42-
);
43+
// Use valid (non-expired) ENS cache for security
44+
const ensCache = useSelector(selectValidEnsCache);
4345
const {
4446
activeNetwork: { chainId, currency, apiUrl },
4547
} = useSelector((state: RootState) => state.vault);

source/pages/Send/Confirm.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import { useUtils, usePrice } from 'hooks/index';
2525
import { useController } from 'hooks/useController';
2626
import { useEIP1559 } from 'hooks/useEIP1559';
2727
import { RootState } from 'state/store';
28-
import { selectEnsNameToAddress } from 'state/vault/selectors';
28+
import {
29+
selectEnsNameToAddress,
30+
selectValidEnsCache,
31+
} from 'state/vault/selectors';
2932
import { INetworkType } from 'types/network';
3033
import { handleTransactionError } from 'utils/errorHandling';
3134
import { formatGweiValue } from 'utils/formatSyscoinValue';
@@ -61,9 +64,8 @@ export const SendConfirm = () => {
6164
);
6265
const { fiat } = useSelector((state: RootState) => state.price);
6366
const activeAccount = accounts[activeAccountMeta.type][activeAccountMeta.id];
64-
const ensCache = useSelector(
65-
(state: RootState) => state.vaultGlobal.ensCache
66-
);
67+
// Use valid (non-expired) ENS cache for security
68+
const ensCache = useSelector(selectValidEnsCache);
6769
// when using the default routing, state will have the tx data
6870
// when using createPopup (DApps), the data comes from route params
6971
const location = useLocation();

source/pages/Send/SendEth.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import { useUtils } from 'hooks/index';
2121
import { useAdjustedExplorer } from 'hooks/useAdjustedExplorer';
2222
import { useController } from 'hooks/useController';
2323
import { RootState } from 'state/store';
24-
import { selectEnsNameToAddress } from 'state/vault/selectors';
25-
import { selectActiveAccountWithAssets } from 'state/vault/selectors';
24+
import {
25+
selectEnsNameToAddress,
26+
selectActiveAccountWithAssets,
27+
selectValidEnsCache,
28+
} from 'state/vault/selectors';
2629
import { ITokenEthProps } from 'types/tokens';
2730
import {
2831
getAssetBalance,
@@ -106,9 +109,8 @@ export const SendEth = () => {
106109
>([]);
107110

108111
const accounts = useSelector((state: RootState) => state.vault.accounts);
109-
const ensCache = useSelector(
110-
(state: RootState) => state.vaultGlobal.ensCache
111-
);
112+
// Use valid (non-expired) ENS cache for security
113+
const ensCache = useSelector(selectValidEnsCache);
112114
const ensNameToAddress = useSelector(selectEnsNameToAddress);
113115
const vaultActiveAccount = useSelector(
114116
(state: RootState) => state.vault.activeAccount

source/pages/Send/components/TransactionDetails.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Tooltip } from 'components/Tooltip';
1212
import { useController } from 'hooks/useController';
1313
import { useUtils } from 'hooks/useUtils';
1414
import { RootState } from 'state/store';
15+
import { selectValidEnsCache } from 'state/vault/selectors';
1516
import { IBlacklistCheckResult } from 'types/security';
1617
import {
1718
ICustomFeeParams,
@@ -64,7 +65,8 @@ export const TransactionDetailsComponent = (
6465
(state: RootState) => state.vault.activeNetwork
6566
);
6667
// Prefer rendering ENS name when available in cache for destination
67-
const ensCache = useReduxSelector((s: RootState) => s.vaultGlobal.ensCache);
68+
// Use valid (non-expired) ENS cache for security
69+
const ensCache = useReduxSelector(selectValidEnsCache);
6870

6971
// Helper function to get appropriate copy message based on field type
7072
const getCopyMessage = (fieldType: 'address' | 'hash' | 'other') => {

source/state/vault/selectors.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createSelector } from '@reduxjs/toolkit';
22

33
import { RootState } from 'state/store';
4+
import { ENS_CACHE_TTL_MS } from 'state/vaultGlobal';
45

56
import { IAccountAssets, IAccountTransactions } from './types';
67

@@ -85,17 +86,50 @@ export const selectActiveAccountAndVaultData = createSelector(
8586
(account, assets, vaultData) => ({ account, assets, ...vaultData })
8687
);
8788

88-
// ENS selectors
89+
// ENS selectors with TTL expiration support
8990
export const selectEnsCache = (state: RootState) => state.vaultGlobal.ensCache;
9091

91-
// Derived map: nameLower -> addressLower, built once and memoized
92-
export const selectEnsNameToAddress = createSelector(
92+
/**
93+
* Helper to check if an ENS cache entry is still valid (not expired)
94+
* @param entry - The cache entry with timestamp
95+
* @param now - Current timestamp (defaults to Date.now())
96+
* @returns true if the entry is still valid
97+
*/
98+
export const isEnsCacheEntryValid = (
99+
entry: { name: string; timestamp: number },
100+
now: number = Date.now()
101+
): boolean => now - entry.timestamp < ENS_CACHE_TTL_MS;
102+
103+
/**
104+
* Selector that returns only non-expired ENS cache entries
105+
* This ensures stale ENS lookups are not used for security
106+
*/
107+
export const selectValidEnsCache = createSelector(
93108
[selectEnsCache],
94109
(ensCache) => {
110+
if (!ensCache) return {};
111+
const now = Date.now();
112+
const validCache: typeof ensCache = {};
113+
114+
for (const [addrLower, entry] of Object.entries(ensCache)) {
115+
if (isEnsCacheEntryValid(entry, now)) {
116+
validCache[addrLower] = entry;
117+
}
118+
}
119+
120+
return validCache;
121+
}
122+
);
123+
124+
// Derived map: nameLower -> addressLower, built once and memoized
125+
// Only includes non-expired entries for security
126+
export const selectEnsNameToAddress = createSelector(
127+
[selectValidEnsCache],
128+
(validEnsCache) => {
95129
const map: Record<string, string> = {};
96-
if (!ensCache) return map;
130+
if (!validEnsCache) return map;
97131
try {
98-
for (const [addrLower, v] of Object.entries(ensCache as any)) {
132+
for (const [addrLower, v] of Object.entries(validEnsCache as any)) {
99133
const nameLower = String((v as any)?.name || '').toLowerCase();
100134
if (nameLower) map[nameLower] = addrLower;
101135
}

source/state/vaultGlobal/index.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { IGlobalState } from '../vault/types';
44
import { INetwork, INetworkType } from 'types/network';
55
import { PALI_NETWORKS_STATE } from 'utils/constants';
66

7+
// ENS cache TTL: 5 minutes (300,000 ms)
8+
// ENS names can change, so we don't cache them indefinitely for security
9+
export const ENS_CACHE_TTL_MS = 5 * 60 * 1000;
10+
711
const initialState: IGlobalState = {
812
activeSlip44: null,
913
advancedSettings: {
@@ -122,10 +126,42 @@ const vaultGlobalSlice = createSlice({
122126
) {
123127
if (!state.ensCache) state.ensCache = {};
124128
const addressLower = action.payload.address.toLowerCase();
125-
state.ensCache[addressLower] = {
129+
const now = Date.now();
130+
131+
// Clean up expired entries while adding new one (opportunistic cleanup)
132+
const cleanedCache: typeof state.ensCache = {};
133+
for (const [addr, entry] of Object.entries(state.ensCache)) {
134+
if (now - entry.timestamp < ENS_CACHE_TTL_MS) {
135+
cleanedCache[addr] = entry;
136+
}
137+
}
138+
139+
// Add the new entry
140+
cleanedCache[addressLower] = {
126141
name: action.payload.name,
127-
timestamp: Date.now(),
142+
timestamp: now,
128143
};
144+
145+
state.ensCache = cleanedCache;
146+
},
147+
clearExpiredEnsCache(state: IGlobalState) {
148+
// Clear all expired ENS cache entries
149+
if (!state.ensCache) return;
150+
151+
const now = Date.now();
152+
const cleanedCache: typeof state.ensCache = {};
153+
154+
for (const [addr, entry] of Object.entries(state.ensCache)) {
155+
if (now - entry.timestamp < ENS_CACHE_TTL_MS) {
156+
cleanedCache[addr] = entry;
157+
}
158+
}
159+
160+
state.ensCache = cleanedCache;
161+
},
162+
clearAllEnsCache(state: IGlobalState) {
163+
// Clear the entire ENS cache (useful for security-sensitive operations)
164+
state.ensCache = {};
129165
},
130166
setIsSwitchingAccount(state: IGlobalState, action: PayloadAction<boolean>) {
131167
state.isSwitchingAccount = action.payload;
@@ -332,6 +368,8 @@ export const {
332368
resetNetworkQualityForNewNetwork,
333369
setPostNetworkSwitchLoading,
334370
setEnsName,
371+
clearExpiredEnsCache,
372+
clearAllEnsCache,
335373
} = vaultGlobalSlice.actions;
336374

337375
export default vaultGlobalSlice.reducer;

0 commit comments

Comments
 (0)