跳到主要内容

Web3前端安全最佳实践

Web3前端开发面临着独特的安全挑战,涉及用户资产安全、数据隐私和智能合约交互等关键问题。一个小小的安全漏洞可能导致用户资产损失和信任崩塌。本章将系统介绍Web3前端开发中的安全最佳实践,帮助开发者构建更安全、更可靠的DApp。

Web3前端安全概述

Web3前端应用与传统Web应用在安全方面存在显著差异。传统Web应用的安全关注点主要是数据泄露和服务中断,而Web3应用还需要保护用户的数字资产和私钥安全。

Web3前端特有的安全挑战

  1. 直接资产交互:用户直接在前端与区块链交互,涉及真实的加密货币和数字资产
  2. 钱包集成风险:与外部钱包的集成引入了额外的攻击面
  3. 智能合约漏洞:前端应用可能成为利用智能合约漏洞的入口
  4. 链上数据透明度:区块链的透明性意味着所有交易都是公开可见的
  5. 复杂的用户认知:用户可能缺乏Web3安全知识,增加了社会工程学攻击风险

安全威胁矩阵

威胁类型描述潜在影响
钓鱼攻击模仿合法DApp,诱导用户连接钱包并授权操作用户资产被盗、个人信息泄露
恶意合约交互诱导用户与恶意智能合约交互资产被盗、授权被滥用
中间人攻击拦截并修改前端与区块链节点之间的通信交易被篡改、资产被重定向
钱包授权滥用过度请求钱包权限或滥用已授权的权限资金被转移、NFT被转移
跨站脚本(XSS)注入恶意脚本,窃取用户的钱包信息钱包签名被劫持、交易被伪造
点击劫持诱导用户在不知情的情况下点击恶意按钮执行未经授权的交易
DNS劫持篡改DNS解析,将用户引导至恶意网站全面接管用户交互
供应链攻击恶意依赖库或组件被引入项目代码执行、数据窃取

钱包连接与授权安全

钱包连接是用户与DApp交互的第一步,也是安全防护的第一道防线。

安全的钱包连接实现

// 安全的钱包连接工具函数
async function connectWalletSafely(options = {}) {
try {
// 验证环境是否支持Web3
if (!window.ethereum && !window.web3) {
throw new Error('未检测到Web3钱包,请安装MetaMask等钱包插件');
}

// 显示钱包连接前的安全提示
const userAcknowledged = showSecurityNoticeBeforeConnect();
if (!userAcknowledged) {
throw new Error('用户取消了钱包连接');
}

// 检查当前网站是否为HTTPS
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
console.warn('警告:在非HTTPS环境下使用钱包存在安全风险');
showWarningForInsecureConnection();
}

// 限制仅在用户主动交互时连接钱包
if (!options.initiatedByUserInteraction) {
throw new Error('钱包连接必须由用户主动触发');
}

// 请求账户访问权限
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
}).catch(err => {
if (err.code === 4001) {
throw new Error('用户拒绝了钱包连接请求');
}
throw err;
});

// 验证返回的账户格式
if (!Array.isArray(accounts) || accounts.length === 0 || !isValidEthereumAddress(accounts[0])) {
throw new Error('获取的账户信息无效');
}

// 记录连接日志(可选,但有助于安全审计)
logWalletConnection(accounts[0], options.walletType || 'unknown');

// 订阅钱包事件以监控异常行为
setupWalletEventListeners();

return {
success: true,
account: accounts[0],
provider: window.ethereum
};
} catch (error) {
console.error('钱包连接失败:', error);
// 显示用户友好的错误信息,避免暴露技术细节
showUserFriendlyError('钱包连接失败,请检查钱包是否正常运行并尝试重新连接');

return {
success: false,
error: error.message
};
}
}

// 验证以太坊地址格式
function isValidEthereumAddress(address) {
// 以太坊地址格式验证正则表达式
const addressRegex = /^0x[a-fA-F0-9]{40}$/;
return addressRegex.test(address);
}

// 钱包连接前的安全提示
function showSecurityNoticeBeforeConnect() {
// 在实际应用中,应显示一个包含安全提示的模态框
console.log('安全提示:请确认您正在连接到官方DApp网站,不要在不信任的网站上连接钱包。');
// 简化实现,默认返回true
return true;
}

// 设置钱包事件监听器
function setupWalletEventListeners() {
if (!window.ethereum) return;

// 监听账户变化
window.ethereum.on('accountsChanged', (accounts) => {
console.log('账户已更改:', accounts);
// 验证新账户是否有效
if (accounts.length > 0 && !isValidEthereumAddress(accounts[0])) {
console.warn('检测到无效的账户地址,可能存在安全风险');
showSecurityAlert('检测到异常账户变更,请确认您的钱包安全性');
}
// 更新应用状态
handleAccountsChanged(accounts);
});

// 监听网络变化
window.ethereum.on('chainChanged', (chainId) => {
console.log('网络已更改:', chainId);
// 验证网络变更是否符合预期
verifyNetworkChange(chainId);
// 更新应用状态
handleChainChanged(chainId);
});

// 监听连接断开
window.ethereum.on('disconnect', (error) => {
console.log('钱包连接已断开:', error);
// 清理应用状态
handleDisconnect(error);
});
}

// 使用示例(React组件)
function WalletConnectButton() {
const [isConnecting, setIsConnecting] = useState(false);

const handleConnectClick = async () => {
// 标记连接请求由用户交互触发
const options = { initiatedByUserInteraction: true, walletType: 'MetaMask' };

setIsConnecting(true);
try {
const result = await connectWalletSafely(options);

if (result.success) {
console.log('钱包连接成功,账户:', result.account);
// 更新全局状态或执行其他操作
}
} catch (error) {
console.error('连接过程中出错:', error);
} finally {
setIsConnecting(false);
}
};

return (
<button
onClick={handleConnectClick}
disabled={isConnecting}
className="wallet-connect-button"
>
{isConnecting ? '连接中...' : '连接钱包'}
</button>
);
}

授权权限最小化原则

遵循最小权限原则,只请求必要的权限,避免过度授权。

// 智能合约交互权限管理
class ContractPermissionManager {
constructor() {
this.approvedContracts = new Map(); // 存储已授权的合约和权限
}

// 请求合约授权
async requestContractPermission(contractAddress, contractAbi, permissionType, options = {}) {
try {
// 验证合约地址
if (!isValidEthereumAddress(contractAddress)) {
throw new Error('无效的合约地址');
}

// 检查是否已获得相同权限
const existingPermission = this.getExistingPermission(contractAddress, permissionType);
if (existingPermission && !options.forceReRequest) {
console.log('已拥有该合约的相同权限,无需重新请求');
return { success: true, existingPermission: true };
}

// 显示权限请求前的安全提示
const userConfirmed = await showPermissionRequestNotice(contractAddress, permissionType, options);
if (!userConfirmed) {
throw new Error('用户取消了权限请求');
}

// 根据权限类型执行相应的授权操作
let approvalResult;
switch (permissionType) {
case 'token_spend':
approvalResult = await this.requestTokenSpendApproval(contractAddress, contractAbi, options);
break;
case 'contract_interaction':
// 对于普通合约交互,不需要预先授权
approvalResult = { success: true };
break;
case 'delegate_vote':
approvalResult = await this.requestDelegationApproval(contractAddress, contractAbi, options);
break;
default:
throw new Error('不支持的权限类型');
}

if (approvalResult.success) {
// 记录授权信息
this.recordPermission(contractAddress, permissionType, options);
// 记录授权日志
logPermissionGranted(contractAddress, permissionType, options);
}

return approvalResult;
} catch (error) {
console.error('请求合约权限失败:', error);
// 显示用户友好的错误信息
showPermissionError(error.message);
return { success: false, error: error.message };
}
}

// 请求代币花费授权
async requestTokenSpendApproval(contractAddress, contractAbi, options) {
try {
const web3 = new Web3(window.ethereum);
const tokenContract = new web3.eth.Contract(contractAbi, contractAddress);
const spenderAddress = options.spenderAddress || window.location.origin;
const amount = options.amount || '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; // 无限授权

// 构建授权交易
const approveTx = tokenContract.methods.approve(spenderAddress, amount);

// 估算Gas
const gas = await approveTx.estimateGas({ from: options.userAddress });

// 显示详细的授权确认信息
const userConfirmed = await showDetailedApprovalConfirmation({
contractAddress,
spenderAddress,
amount,
gas,
tokenName: options.tokenName || 'Token',
isInfiniteApproval: options.amount === undefined
});

if (!userConfirmed) {
throw new Error('用户取消了授权');
}

// 发送授权交易
const txHash = await approveTx.send({
from: options.userAddress,
gas
});

return { success: true, txHash };
} catch (error) {
console.error('代币授权失败:', error);
throw error;
}
}

// 记录权限信息
recordPermission(contractAddress, permissionType, options) {
const permissionData = {
timestamp: Date.now(),
type: permissionType,
options: { ...options },
expiresAt: options.expiry ? Date.now() + options.expiry : null
};

// 存储权限信息(内存中,不持久化敏感数据)
let contractPermissions = this.approvedContracts.get(contractAddress) || [];
contractPermissions.push(permissionData);
this.approvedContracts.set(contractAddress, contractPermissions);

// 限制存储的权限记录数量
this.limitPermissionHistory(contractAddress);
}

// 获取已存在的权限
getExistingPermission(contractAddress, permissionType) {
const contractPermissions = this.approvedContracts.get(contractAddress);
if (!contractPermissions) return null;

// 查找未过期的相同类型权限
return contractPermissions.find(permission =>
permission.type === permissionType &&
(!permission.expiresAt || permission.expiresAt > Date.now())
) || null;
}

// 限制权限历史记录数量
limitPermissionHistory(contractAddress) {
const MAX_HISTORY = 10;
const contractPermissions = this.approvedContracts.get(contractAddress);

if (contractPermissions && contractPermissions.length > MAX_HISTORY) {
// 保留最新的记录
this.approvedContracts.set(contractAddress, contractPermissions.slice(-MAX_HISTORY));
}
}

// 撤销合约授权
async revokeContractPermission(contractAddress, permissionType, options = {}) {
// 实现撤销授权的逻辑
// ...
}

// 清除所有权限记录
clearAllPermissionRecords() {
this.approvedContracts.clear();
console.log('所有权限记录已清除');
}
}

// 使用示例
const permissionManager = new ContractPermissionManager();

// 请求代币授权(有限金额)
const result = await permissionManager.requestContractPermission(
'0x1234567890123456789012345678901234567890', // 代币合约地址
erc20Abi, // ERC20代币ABI
'token_spend',
{
userAddress: currentUserAddress,
spenderAddress: '0x0987654321098765432109876543210987654321', // 接收授权的合约地址
amount: web3.utils.toWei('100', 'ether'), // 授权金额
tokenName: 'Example Token',
forceReRequest: false // 是否强制重新请求
}
);

防止钱包授权滥用

实现机制防止钱包授权被滥用,特别是无限授权的情况。

// 授权监控与保护服务
class ApprovalGuardService {
constructor() {
this.maxApprovalAmounts = new Map(); // 存储各代币的最大允许授权金额
this.suspiciousApprovalThreshold = 10000; // 可疑授权阈值(代币单位)
this.recentApprovals = []; // 最近的授权记录
}

// 初始化授权保护规则
initProtectionRules(customRules = {}) {
// 设置默认保护规则
const defaultRules = {
maxApprovalAmounts: {
'ETH': web3.utils.toWei('10', 'ether'),
'USDC': web3.utils.toWei('10000', 'mwei'),
'USDT': web3.utils.toWei('10000', 'mwei')
},
blockInfiniteApprovals: true,
approvalConfirmationThreshold: web3.utils.toWei('1000', 'ether'),
monitorApprovalChanges: true
};

// 合并自定义规则
this.rules = { ...defaultRules, ...customRules };

// 应用规则
this.applyProtectionRules();
}

// 应用保护规则
applyProtectionRules() {
// 存储最大允许授权金额
if (this.rules.maxApprovalAmounts) {
for (const [tokenSymbol, maxAmount] of Object.entries(this.rules.maxApprovalAmounts)) {
this.maxApprovalAmounts.set(tokenSymbol, maxAmount);
}
}
}

// 检查授权请求是否安全
async checkApprovalSafety(approvalRequest) {
try {
const {
contractAddress,
spenderAddress,
amount,
tokenSymbol,
userAddress
} = approvalRequest;

// 1. 检查是否为无限授权
if (this.rules.blockInfiniteApprovals) {
const isInfiniteApproval = this.isInfiniteApproval(amount);
if (isInfiniteApproval) {
const userConfirmed = await showInfiniteApprovalWarning({
tokenSymbol,
spenderAddress
});

if (!userConfirmed) {
return {
isSafe: false,
reason: '用户拒绝了无限授权',
recommendation: '使用有限金额授权'
};
}
}
}

// 2. 检查授权金额是否超过最大值
const maxAllowedAmount = this.maxApprovalAmounts.get(tokenSymbol);
if (maxAllowedAmount && this.compareAmounts(amount, maxAllowedAmount) > 0) {
const userConfirmed = await showHighApprovalAmountWarning({
tokenSymbol,
requestedAmount: amount,
maxAllowedAmount: maxAllowedAmount
});

if (!userConfirmed) {
return {
isSafe: false,
reason: '用户拒绝了高额授权',
recommendation: `授权金额不超过 ${tokenSymbol} ${this.formatAmount(maxAllowedAmount, tokenSymbol)}`
};
}
}

// 3. 检查授权是否超过确认阈值
if (this.rules.approvalConfirmationThreshold &&
this.compareAmounts(amount, this.rules.approvalConfirmationThreshold) >= 0) {
// 要求额外的安全确认
const isVerified = await requireExtraSecurityVerification();

if (!isVerified) {
return {
isSafe: false,
reason: '额外的安全验证未通过',
recommendation: '完成安全验证以继续'
};
}
}

// 4. 检查授权对象是否为可疑地址
const isSuspiciousSpender = await checkIfAddressIsSuspicious(spenderAddress);
if (isSuspiciousSpender) {
return {
isSafe: false,
reason: '检测到可疑的授权对象地址',
recommendation: '不要向未知或可疑地址授予权限'
};
}

// 5. 检查是否在短时间内有类似的授权
const recentSimilarApproval = this.findRecentSimilarApproval(approvalRequest);
if (recentSimilarApproval) {
const userConfirmed = await showDuplicateApprovalWarning(recentSimilarApproval);

if (!userConfirmed) {
return {
isSafe: false,
reason: '用户取消了重复授权',
recommendation: '检查是否真的需要重复授权'
};
}
}

// 授权请求通过安全检查
return {
isSafe: true,
reason: '授权请求通过安全检查',
recommendation: '建议定期检查并撤销不需要的授权'
};
} catch (error) {
console.error('检查授权安全性时出错:', error);
// 发生错误时默认阻止授权
return {
isSafe: false,
reason: '安全检查过程中发生错误',
recommendation: '稍后重试或联系客服'
};
}
}

// 判断是否为无限授权
isInfiniteApproval(amount) {
// 以太坊中常见的无限授权值
const infiniteApprovalValues = [
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'115792089237316195423570985008687907853269984665640564039457584007913129639935'
];

return infiniteApprovalValues.includes(amount.toString()) ||
(typeof amount === 'string' && amount.length > 30);
}

// 比较两个金额
compareAmounts(amount1, amount2) {
// 假设amount1和amount2都是十六进制字符串或BigNumber
const web3 = new Web3();
const num1 = web3.utils.toBN(amount1);
const num2 = web3.utils.toBN(amount2);

return num1.cmp(num2); // 返回-1, 0, 或1
}

// 格式化金额显示
formatAmount(amount, tokenSymbol) {
const web3 = new Web3();
try {
// 根据代币符号使用适当的小数位数
const decimals = tokenSymbol === 'ETH' ? 18 :
['USDC', 'USDT', 'DAI'].includes(tokenSymbol) ? 6 : 18;

return parseFloat(web3.utils.fromWei(amount, 'ether')).toFixed(2);
} catch (error) {
return '无法格式化';
}
}

// 查找最近类似的授权
findRecentSimilarApproval(approvalRequest) {
const {
contractAddress,
spenderAddress,
tokenSymbol
} = approvalRequest;

// 时间窗口:5分钟
const timeWindow = 5 * 60 * 1000;
const now = Date.now();

// 在最近的授权记录中查找类似的授权
return this.recentApprovals.find(approval =>
approval.contractAddress === contractAddress &&
approval.spenderAddress === spenderAddress &&
approval.tokenSymbol === tokenSymbol &&
(now - approval.timestamp) < timeWindow
) || null;
}

// 记录授权请求
recordApproval(approvalRequest) {
const approvalRecord = {
...approvalRequest,
timestamp: Date.now(),
id: `approval_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
};

// 添加到最近授权记录
this.recentApprovals.push(approvalRecord);

// 保持记录数量合理(最多50条)
if (this.recentApprovals.length > 50) {
this.recentApprovals.shift();
}

return approvalRecord;
}

// 获取用户的授权管理建议
async getApprovalManagementSuggestions(userAddress) {
try {
// 获取用户所有的代币授权
const userApprovals = await fetchUserTokenApprovals(userAddress);

const suggestions = [];

// 分析每个授权并提供建议
for (const approval of userApprovals) {
// 检查是否为无限授权
if (this.isInfiniteApproval(approval.amount)) {
suggestions.push({
type: 'infinite_approval',
tokenSymbol: approval.tokenSymbol,
contractAddress: approval.contractAddress,
spenderAddress: approval.spenderAddress,
suggestion: '建议将无限授权更改为有限授权',
action: 'reduce_approval'
});
}

// 检查授权金额是否过大
const maxAllowedAmount = this.maxApprovalAmounts.get(approval.tokenSymbol);
if (maxAllowedAmount && this.compareAmounts(approval.amount, maxAllowedAmount) > 0) {
suggestions.push({
type: 'excessive_approval',
tokenSymbol: approval.tokenSymbol,
contractAddress: approval.contractAddress,
spenderAddress: approval.spenderAddress,
currentAmount: approval.amount,
suggestedAmount: maxAllowedAmount,
suggestion: `建议减少 ${approval.tokenSymbol} 的授权金额`,
action: 'reduce_approval'
});
}

// 检查是否为不活跃的DApp授权
if (await isDAppInactive(approval.spenderAddress)) {
suggestions.push({
type: 'inactive_dapp',
tokenSymbol: approval.tokenSymbol,
contractAddress: approval.contractAddress,
spenderAddress: approval.spenderAddress,
suggestion: '检测到该DApp已长时间不活跃,建议撤销授权',
action: 'revoke_approval'
});
}
}

return suggestions;
} catch (error) {
console.error('获取授权管理建议时出错:', error);
return [];
}
}
}

// 使用示例
const approvalGuard = new ApprovalGuardService();
approvalGuard.initProtectionRules({
blockInfiniteApprovals: true,
maxApprovalAmounts: {
'ETH': web3.utils.toWei('5', 'ether'),
'USDC': web3.utils.toWei('5000', 'mwei')
}
});

// 在请求授权前检查安全性
const approvalRequest = {
contractAddress: '0x1234567890123456789012345678901234567890',
spenderAddress: '0x0987654321098765432109876543210987654321',
amount: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', // 无限授权
tokenSymbol: 'ETH',
userAddress: currentUserAddress
};

const safetyCheckResult = await approvalGuard.checkApprovalSafety(approvalRequest);

if (safetyCheckResult.isSafe) {
console.log('授权请求安全,可以继续:', safetyCheckResult.recommendation);
// 继续授权流程
approvalGuard.recordApproval(approvalRequest);
} else {
console.warn('授权请求存在风险:', safetyCheckResult.reason);
showSecurityWarning(safetyCheckResult.reason, safetyCheckResult.recommendation);
// 阻止授权流程
}

智能合约交互安全

与智能合约交互是Web3前端应用的核心功能,但也带来了独特的安全风险。

合约交互安全验证

// 智能合约交互安全验证器
class ContractInteractionValidator {
constructor() {
this.trustedContracts = new Set(); // 可信合约白名单
this.suspiciousPatterns = [
/transferFrom.*\(msg\.sender/, // 可疑的转账模式
/selfdestruct/, // 自毁指令
/delegatecall/, // 委托调用
/address\(0\)/ // 零地址引用
];
}

// 添加可信合约
addTrustedContract(contractAddress, contractName = '') {
if (isValidEthereumAddress(contractAddress)) {
this.trustedContracts.add(contractAddress.toLowerCase());
console.log(`已添加可信合约: ${contractName || contractAddress}`);
} else {
console.error('无效的合约地址,无法添加到可信列表');
}
}

// 批量添加可信合约
addTrustedContracts(contracts) {
contracts.forEach(contract => {
if (typeof contract === 'object') {
this.addTrustedContract(contract.address, contract.name);
} else if (typeof contract === 'string') {
this.addTrustedContract(contract);
}
});
}

// 验证合约交互安全性
async validateContractInteraction(contractAddress, methodName, params, options = {}) {
try {
const normalizedAddress = contractAddress.toLowerCase();

// 1. 检查合约是否在可信列表中
const isTrustedContract = this.trustedContracts.has(normalizedAddress);

// 2. 如果不是可信合约,进行更详细的检查
if (!isTrustedContract && !options.skipVerification) {
// a. 验证合约代码是否存在(非空)
const hasCode = await this.checkContractCodeExists(normalizedAddress);
if (!hasCode) {
return {
isValid: false,
reason: '目标地址没有合约代码,可能是外部账户',
severity: 'high',
isTrusted: false
};
}

// b. 检查合约创建时间(新创建的合约需额外谨慎)
const contractAge = await this.getContractAge(normalizedAddress);
if (contractAge && contractAge < 24 * 60 * 60 * 1000) { // 小于24小时
const userConfirmed = await showNewContractWarning({
contractAddress,
ageInHours: Math.floor(contractAge / (60 * 60 * 1000))
});

if (!userConfirmed) {
return {
isValid: false,
reason: '用户取消了与新合约的交互',
severity: 'medium',
isTrusted: false
};
}
}

// c. 检查合约是否存在可疑模式(如果提供了ABI或签名)
if (options.abi || options.signature) {
const hasSuspiciousPattern = this.checkForSuspiciousPatterns(options.abi || options.signature);
if (hasSuspiciousPattern) {
return {
isValid: false,
reason: '检测到合约中包含可疑代码模式',
severity: 'high',
isTrusted: false,
suspiciousPatterns: hasSuspiciousPattern
};
}
}

// d. 检查合约是否存在于已知的恶意合约列表
const isMalicious = await this.checkIfContractIsMalicious(normalizedAddress);
if (isMalicious) {
return {
isValid: false,
reason: '检测到目标合约在已知的恶意合约列表中',
severity: 'critical',
isTrusted: false
};
}

// e. 如果是高风险操作,要求额外验证
if (this.isHighRiskOperation(methodName, params)) {
const extraVerified = await requireExtraVerificationForHighRiskOperation({
contractAddress,
methodName,
params
});

if (!extraVerified) {
return {
isValid: false,
reason: '高风险操作验证未通过',
severity: 'high',
isTrusted: false
};
}
}
}

// 3. 记录合约交互日志
this.logContractInteraction({
contractAddress: normalizedAddress,
methodName,
params,
isTrusted: isTrustedContract,
timestamp: Date.now(),
userAddress: options.userAddress
});

// 4. 返回验证结果
return {
isValid: true,
reason: isTrustedContract ? '合约在可信列表中' : '合约通过安全验证',
severity: 'low',
isTrusted: isTrustedContract
};
} catch (error) {
console.error('验证合约交互安全性时出错:', error);
// 发生错误时默认阻止交互,除非明确设置忽略错误
if (options.ignoreErrors) {
return {
isValid: true,
reason: '验证出错,但根据设置忽略错误',
severity: 'medium',
isTrusted: false
};
}

return {
isValid: false,
reason: '验证过程中发生错误',
severity: 'medium',
isTrusted: false
};
}
}

// 检查合约代码是否存在
async checkContractCodeExists(contractAddress) {
try {
const web3 = new Web3(window.ethereum);
const code = await web3.eth.getCode(contractAddress);
// 合约代码不为'0x'(空)表示存在代码
return code !== '0x';
} catch (error) {
console.error('检查合约代码时出错:', error);
return false;
}
}

// 获取合约年龄(创建时间)
async getContractAge(contractAddress) {
try {
// 注意:获取合约创建时间需要访问区块链历史数据,可能需要使用第三方API
// 这里使用Etherscan API作为示例(需要API密钥)
const apiKey = getEtherscanApiKey(); // 获取配置的API密钥
const response = await fetch(`https://api.etherscan.io/api?module=account&action=txlistinternal&address=${contractAddress}&startblock=0&endblock=99999999&sort=asc&apikey=${apiKey}`);

if (!response.ok) {
throw new Error('Etherscan API请求失败');
}

const data = await response.json();

if (data.status === '1' && data.result && data.result.length > 0) {
// 查找合约创建交易
const creationTx = data.result.find(tx => tx.type === 'create');
if (creationTx) {
const creationTime = parseInt(creationTx.timeStamp) * 1000; // 转换为毫秒
return Date.now() - creationTime;
}
}

return null; // 无法确定合约年龄
} catch (error) {
console.error('获取合约年龄时出错:', error);
return null; // 出错时返回null
}
}

// 检查是否包含可疑代码模式
checkForSuspiciousPatterns(abiOrSignature) {
const suspiciousPatternsFound = [];

// 将ABI或签名转换为字符串以便检查
const abiString = JSON.stringify(abiOrSignature);

// 检查每个可疑模式
for (const pattern of this.suspiciousPatterns) {
if (pattern.test(abiString)) {
suspiciousPatternsFound.push(pattern.toString());
}
}

return suspiciousPatternsFound.length > 0 ? suspiciousPatternsFound : false;
}

// 检查合约是否为恶意合约
async checkIfContractIsMalicious(contractAddress) {
try {
// 这是一个简化实现,实际应用中应使用更完善的恶意合约检测服务
// 例如:MetaMask的PhishFort、Chainalysis或其他安全服务的API

// 示例实现:检查地址是否在本地黑名单中
const localBlacklist = getLocalContractBlacklist();
if (localBlacklist.includes(contractAddress.toLowerCase())) {
return true;
}

// 检查是否是高风险合约(如蜜罐合约)的特征
const hasHoneypotFeatures = await this.checkForHoneypotFeatures(contractAddress);
if (hasHoneypotFeatures) {
return true;
}

return false;
} catch (error) {
console.error('检查恶意合约时出错:', error);
// 出错时默认返回false,避免误报
return false;
}
}

// 检查是否为高风险操作
isHighRiskOperation(methodName, params) {
// 常见的高风险操作方法名
const highRiskMethods = [
'transfer', 'transferFrom', 'send', 'approve', 'increaseAllowance',
'decreaseAllowance', 'withdraw', 'claim', 'unstake', 'burn',
'renounceOwnership', 'transferOwnership', 'setApprovalForAll'
];

// 检查方法名是否在高风险列表中
const methodNameLower = methodName.toLowerCase();
const isHighRiskMethod = highRiskMethods.some(method =>
methodNameLower.includes(method)
);

// 检查参数中是否包含大额数值或敏感地址
const hasSensitiveParams = this.hasSensitiveParameters(params);

return isHighRiskMethod || hasSensitiveParams;
}

// 检查参数是否包含敏感内容
hasSensitiveParameters(params) {
if (!params || !Array.isArray(params)) return false;

// 检查每个参数
for (const param of params) {
// 检查是否为地址参数且为零地址
if (typeof param === 'string' &&
param.startsWith('0x') &&
param.toLowerCase() === '0x0000000000000000000000000000000000000000') {
return true;
}

// 检查是否为数值参数且数值异常大
if (this.isAbnormallyLargeValue(param)) {
return true;
}
}

return false;
}

// 检查值是否异常大
isAbnormallyLargeValue(value) {
try {
const web3 = new Web3();
const numericValue = web3.utils.toBN(value);
const threshold = web3.utils.toBN(web3.utils.toWei('10000', 'ether'));

return numericValue.gt(threshold);
} catch (error) {
// 无法转换为数值,不是异常大的值
return false;
}
}

// 记录合约交互日志
logContractInteraction(interactionData) {
try {
// 在实际应用中,可能需要将日志发送到服务器进行安全审计
console.log('合约交互日志:', interactionData);

// 本地存储最近的交互记录(可选,用于快速参考)
this.storeRecentInteraction(interactionData);
} catch (error) {
console.error('记录合约交互日志时出错:', error);
// 日志记录失败不应影响主流程
}
}

// 存储最近的交互记录
storeRecentInteraction(interactionData) {
try {
const recentInteractions = JSON.parse(localStorage.getItem('recent_contract_interactions') || '[]');

// 添加新记录
recentInteractions.push({
...interactionData,
// 移除可能敏感的参数详情
params: interactionData.params ? 'redacted' : null
});

// 只保留最近50条记录
if (recentInteractions.length > 50) {
recentInteractions.shift();
}

// 存储回本地存储
localStorage.setItem('recent_contract_interactions', JSON.stringify(recentInteractions));
} catch (error) {
console.error('存储最近交互记录时出错:', error);
}
}

// 检查蜜罐合约特征
async checkForHoneypotFeatures(contractAddress) {
// 简化实现,实际应用中应使用专业的蜜罐检测服务
// 蜜罐合约通常具有以下特征:
// 1. 只能存入代币,无法取出
// 2. 只能由创建者取出
// 3. 交易有特殊的触发条件

// 这里仅作为示例,实际检测需要更复杂的逻辑
return false;
}
}

// 使用示例
const contractValidator = new ContractInteractionValidator();

// 添加可信合约
contractValidator.addTrustedContracts([
{
address: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', // Uniswap V2 Router
name: 'Uniswap V2 Router'
},
{
address: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Uniswap V3 Router
name: 'Uniswap V3 Router'
}
// 其他可信合约...
]);

// 在与合约交互前进行验证
async function safeContractInteraction(contractAddress, methodName, params, abi) {
// 验证合约交互安全性
const validationResult = await contractValidator.validateContractInteraction(
contractAddress,
methodName,
params,
{
abi: abi,
userAddress: currentUserAddress,
skipVerification: false // 不要跳过验证
}
);

if (!validationResult.isValid) {
console.error('合约交互验证失败:', validationResult.reason);
showContractInteractionError(validationResult.reason, validationResult.severity);
throw new Error(`合约交互验证失败: ${validationResult.reason}`);
}

console.log('合约交互验证通过:', validationResult.reason);

// 显示合约交互确认对话框(包含安全信息)
const userConfirmed = await showContractInteractionConfirmation({
contractAddress,
methodName,
params,
isTrusted: validationResult.isTrusted,
validationResult
});

if (!userConfirmed) {
throw new Error('用户取消了合约交互');
}

// 执行合约交互
const web3 = new Web3(window.ethereum);
const contract = new web3.eth.Contract(abi, contractAddress);

try {
// 估算Gas
const gas = await contract.methods[methodName](...params).estimateGas({
from: currentUserAddress
});

// 发送交易
const txHash = await contract.methods[methodName](...params).send({
from: currentUserAddress,
gas
});

return txHash;
} catch (error) {
console.error('合约交互执行失败:', error);
throw error;
}
}

// 使用安全交互函数进行代币转账
const txHash = await safeContractInteraction(
'0x1234567890123456789012345678901234567890', // 代币合约地址
'transfer',
['0x0987654321098765432109876543210987654321', web3.utils.toWei('100', 'ether')],
erc20Abi // ERC20代币ABI
);

防止恶意合约交互

实现多层防护机制,防止用户与恶意合约进行交互。

// 恶意合约交互防护系统
class MaliciousContractProtectionSystem {
constructor() {
this.enabled = true;
this.protectionLevels = {
basic: true, // 基础防护:地址格式检查、黑名单检查
standard: true, // 标准防护:合约代码检查、创建时间检查
advanced: false // 高级防护:代码模式分析、行为分析
};
this.contractRiskCache = new Map(); // 合约风险评估缓存
this.cacheTimeout = 30 * 60 * 1000; // 缓存30分钟
}

// 设置防护级别
setProtectionLevel(level) {
switch (level) {
case 'basic':
this.protectionLevels = {
basic: true,
standard: false,
advanced: false
};
break;
case 'standard':
this.protectionLevels = {
basic: true,
standard: true,
advanced: false
};
break;
case 'advanced':
this.protectionLevels = {
basic: true,
standard: true,
advanced: true
};
break;
case 'off':
this.enabled = false;
break;
default:
console.warn('未知的防护级别,使用标准防护');
this.setProtectionLevel('standard');
}

console.log(`恶意合约防护级别已设置为: ${level}`);
}

// 检查合约风险
async checkContractRisk(contractAddress) {
if (!this.enabled || !isValidEthereumAddress(contractAddress)) {
return { riskLevel: 'unknown', reason: '无效的合约地址或防护已禁用' };
}

const normalizedAddress = contractAddress.toLowerCase();

// 检查缓存
const cachedRisk = this.getCachedRisk(normalizedAddress);
if (cachedRisk) {
return cachedRisk;
}

try {
// 初始化风险评估结果
const riskAssessment = {
address: normalizedAddress,
riskLevel: 'low',
reasons: [],
timestamp: Date.now()
};

// 执行基础防护检查
if (this.protectionLevels.basic) {
const basicChecks = await this.performBasicChecks(normalizedAddress);
if (basicChecks.riskLevel > riskAssessment.riskLevel) {
riskAssessment.riskLevel = basicChecks.riskLevel;
}
riskAssessment.reasons = riskAssessment.reasons.concat(basicChecks.reasons);

// 如果基础检查发现高风险,直接返回
if (riskAssessment.riskLevel === 'high' || riskAssessment.riskLevel === 'critical') {
this.cacheRiskAssessment(normalizedAddress, riskAssessment);
return riskAssessment;
}
}

// 执行标准防护检查
if (this.protectionLevels.standard) {
const standardChecks = await this.performStandardChecks(normalizedAddress);
if (standardChecks.riskLevel > riskAssessment.riskLevel) {
riskAssessment.riskLevel = standardChecks.riskLevel;
}
riskAssessment.reasons = riskAssessment.reasons.concat(standardChecks.reasons);

// 如果标准检查发现高风险,直接返回
if (riskAssessment.riskLevel === 'high' || riskAssessment.riskLevel === 'critical') {
this.cacheRiskAssessment(normalizedAddress, riskAssessment);
return riskAssessment;
}
}

// 执行高级防护检查
if (this.protectionLevels.advanced) {
const advancedChecks = await this.performAdvancedChecks(normalizedAddress);
if (advancedChecks.riskLevel > riskAssessment.riskLevel) {
riskAssessment.riskLevel = advancedChecks.riskLevel;
}
riskAssessment.reasons = riskAssessment.reasons.concat(advancedChecks.reasons);
}

// 缓存风险评估结果
this.cacheRiskAssessment(normalizedAddress, riskAssessment);

return riskAssessment;
} catch (error) {
console.error('评估合约风险时出错:', error);
// 发生错误时返回保守的评估结果
return {
riskLevel: 'medium',
reason: '风险评估过程中发生错误,建议谨慎处理',
error: error.message
};
}
}

// 执行基础防护检查
async performBasicChecks(contractAddress) {
const reasons = [];
let riskLevel = 'low';

// 1. 检查是否为黑名单地址
if (await this.isBlacklistedAddress(contractAddress)) {
reasons.push('合约地址在已知的黑名单中');
riskLevel = 'critical';
}

// 2. 检查是否为零地址
if (contractAddress === '0x0000000000000000000000000000000000000000') {
reasons.push('检测到零地址,这通常是危险的');
riskLevel = 'high';
}

// 3. 检查地址格式是否异常
if (this.hasSuspiciousAddressPattern(contractAddress)) {
reasons.push('地址包含可疑的字符模式');
riskLevel = riskLevel === 'low' ? 'medium' : riskLevel;
}

// 4. 检查是否为最近受到攻击的地址
if (await this.isRecentlyCompromised(contractAddress)) {
reasons.push('合约地址最近可能受到安全攻击');
riskLevel = 'high';
}

return {
riskLevel,
reasons
};
}

// 执行标准防护检查
async performStandardChecks(contractAddress) {
const reasons = [];
let riskLevel = 'low';

// 1. 检查合约是否存在代码
const hasCode = await this.checkContractCodeExists(contractAddress);
if (!hasCode) {
reasons.push('目标地址没有合约代码,可能是外部账户');
riskLevel = 'medium';
} else {
// 2. 检查合约创建时间
const age = await this.getContractAge(contractAddress);
if (age && age < 24 * 60 * 60 * 1000) { // 创建时间小于24小时
reasons.push(`合约创建时间较短(约 ${Math.floor(age / (60 * 60 * 1000))} 小时前)`);
riskLevel = 'medium';
}

// 3. 检查合约交易活跃度
const isActive = await this.checkContractActivity(contractAddress);
if (!isActive) {
reasons.push('合约交易活跃度异常低');
riskLevel = riskLevel === 'low' ? 'medium' : riskLevel;
}

// 4. 检查合约持有资产情况
const hasAbnormalAssets = await this.checkForAbnormalAssets(contractAddress);
if (hasAbnormalAssets) {
reasons.push('合约持有异常数量的资产');
riskLevel = 'high';
}
}

return {
riskLevel,
reasons
};
}

// 执行高级防护检查
async performAdvancedChecks(contractAddress) {
const reasons = [];
let riskLevel = 'low';

// 注意:高级检查可能需要调用第三方安全服务API
// 以下是示例实现

// 1. 检查合约代码是否包含恶意模式
const hasMaliciousPatterns = await this.scanContractForMaliciousPatterns(contractAddress);
if (hasMaliciousPatterns) {
reasons.push('合约代码中检测到潜在的恶意模式');
riskLevel = 'high';
}

// 2. 检查合约是否有审计报告
const hasAudit = await this.checkContractAuditStatus(contractAddress);
if (!hasAudit) {
reasons.push('未找到合约的安全审计报告');
riskLevel = riskLevel === 'low' ? 'medium' : riskLevel;
}

// 3. 检查合约权限控制机制
const hasWeakAccessControl = await this.checkAccessControlMechanisms(contractAddress);
if (hasWeakAccessControl) {
reasons.push('合约可能存在访问控制机制薄弱的问题');
riskLevel = 'high';
}

// 4. 分析合约函数调用图
const hasComplexCallGraph = await this.analyzeContractCallGraph(contractAddress);
if (hasComplexCallGraph) {
reasons.push('合约具有复杂的函数调用图,增加了安全风险');
riskLevel = riskLevel === 'low' ? 'medium' : riskLevel;
}

return {
riskLevel,
reasons
};
}

// 缓存风险评估结果
cacheRiskAssessment(contractAddress, riskAssessment) {
this.contractRiskCache.set(contractAddress, {
...riskAssessment,
cachedAt: Date.now()
});

// 清理过期缓存(可选)
this.cleanupExpiredCache();
}

// 获取缓存的风险评估结果
getCachedRisk(contractAddress) {
const cachedRisk = this.contractRiskCache.get(contractAddress);

if (!cachedRisk) {
return null;
}

// 检查缓存是否过期
const isExpired = Date.now() - cachedRisk.cachedAt > this.cacheTimeout;
if (isExpired) {
this.contractRiskCache.delete(contractAddress);
return null;
}

return cachedRisk;
}

// 清理过期缓存
cleanupExpiredCache() {
const now = Date.now();
for (const [address, cachedRisk] of this.contractRiskCache.entries()) {
if (now - cachedRisk.cachedAt > this.cacheTimeout) {
this.contractRiskCache.delete(address);
}
}
}

// 检查地址是否在黑名单中
async isBlacklistedAddress(address) {
// 简化实现,实际应用中应使用更完善的黑名单服务
const blacklist = await fetchAddressBlacklist();
return blacklist.includes(address.toLowerCase());
}

// 检查地址是否有可疑模式
hasSuspiciousAddressPattern(address) {
// 检查是否有大量重复字符(如0x0000000000000000000000000000000000000001)
const repeatPattern = /(.)\1{10,}/;
return repeatPattern.test(address.replace('0x', ''));
}

// 检查地址是否最近受到攻击
async isRecentlyCompromised(address) {
// 简化实现,实际应用中应使用安全事件监控服务
return false;
}

// 检查合约代码是否存在
async checkContractCodeExists(address) {
const web3 = new Web3(window.ethereum);
const code = await web3.eth.getCode(address);
return code !== '0x';
}

// 获取合约年龄
async getContractAge(address) {
// 实现参考之前的ContractInteractionValidator类
// ...
}

// 检查合约交易活跃度
async checkContractActivity(address) {
// 简化实现,检查过去24小时内是否有交易
try {
const web3 = new Web3(window.ethereum);
const currentBlock = await web3.eth.getBlockNumber();
const blocks24Hours = 60 * 24; // 假设每分钟一个区块
const startBlock = Math.max(0, currentBlock - blocks24Hours);

// 这个方法实际上在Web3.js v1.x中不可用,这里仅作为示例
// 实际应用中应使用Etherscan API或其他区块链API服务
// const transactions = await web3.eth.getTransactionsByAddress(address, startBlock, currentBlock);

// 简化实现:假设所有合约都有一定活跃度
return true;
} catch (error) {
console.error('检查合约活跃度时出错:', error);
return true; // 出错时默认返回true,避免误报
}
}

// 检查合约是否持有异常资产
async checkForAbnormalAssets(address) {
// 简化实现,实际应用中应检查合约持有的各种资产
return false;
}

// 扫描合约中的恶意模式
async scanContractForMaliciousPatterns(address) {
// 简化实现,实际应用中应使用专业的合约安全扫描服务
return false;
}

// 检查合约审计状态
async checkContractAuditStatus(address) {
// 简化实现,实际应用中应查询审计数据库或API
return false;
}

// 检查访问控制机制
async checkAccessControlMechanisms(address) {
// 简化实现,实际应用中应分析合约的权限控制逻辑
return false;
}

// 分析合约调用图
async analyzeContractCallGraph(address) {
// 简化实现,实际应用中应分析合约的函数调用关系
return false;
}

// 清除所有缓存
clearCache() {
this.contractRiskCache.clear();
console.log('合约风险评估缓存已清除');
}
}

// 使用示例
const maliciousContractProtection = new MaliciousContractProtectionSystem();

// 设置防护级别为高级
maliciousContractProtection.setProtectionLevel('advanced');

// 在与未知合约交互前进行风险评估
async function assessContractRiskBeforeInteraction(contractAddress) {
// 评估合约风险
const riskAssessment = await maliciousContractProtection.checkContractRisk(contractAddress);

console.log('合约风险评估结果:', {
address: contractAddress,
riskLevel: riskAssessment.riskLevel,
reasons: riskAssessment.reasons
});

// 根据风险级别采取相应措施
switch (riskAssessment.riskLevel) {
case 'critical':
// 高风险合约,阻止交互
showCriticalRiskWarning(contractAddress, riskAssessment.reasons);
return { allowed: false, reason: '合约风险过高,已阻止交互' };

case 'high':
// 高风险合约,需要用户确认并提供额外警告
const userConfirmed = await showHighRiskWarning(contractAddress, riskAssessment.reasons);
if (!userConfirmed) {
return { allowed: false, reason: '用户取消了高风险合约交互' };
}
// 显示额外的安全提示
showExtraSecurityTips();
return { allowed: true, reason: '用户确认了高风险交互' };

case 'medium':
// 中等风险,显示警告但允许交互
showMediumRiskWarning(contractAddress, riskAssessment.reasons);
return { allowed: true, reason: '合约存在一定风险,已向用户显示警告' };

case 'low':
case 'unknown':
// 低风险或未知风险,正常进行
return { allowed: true, reason: '合约风险可接受' };

default:
// 其他情况,保守处理
showUnknownRiskWarning(contractAddress);
return { allowed: true, reason: '合约风险未知,建议谨慎处理' };
}
}

// 安全的合约交互入口
async function secureContractInteractionEntry(contractAddress, methodName, params, abi) {
try {
// 1. 进行合约风险评估
const riskAssessmentResult = await assessContractRiskBeforeInteraction(contractAddress);
if (!riskAssessmentResult.allowed) {
throw new Error(riskAssessmentResult.reason);
}

// 2. 进行合约交互验证
const validationResult = await contractValidator.validateContractInteraction(
contractAddress,
methodName,
params,
{
abi: abi,
userAddress: currentUserAddress
}
);

if (!validationResult.isValid) {
throw new Error(`合约交互验证失败: ${validationResult.reason}`);
}

// 3. 执行合约交互(具体实现参考前面的代码)
const txHash = await executeContractInteraction(contractAddress, methodName, params, abi);

return {
success: true,
txHash,
riskLevel: riskAssessmentResult.allowed ? 'accepted' : 'rejected'
};
} catch (error) {
console.error('安全合约交互失败:', error);
showUserFriendlyError(error.message);

return {
success: false,
error: error.message
};
}
}

常见Web3前端攻击与防御

跨站脚本(XSS)攻击

跨站脚本攻击是Web3应用中最常见的安全威胁之一,攻击者通过注入恶意脚本窃取用户的钱包信息。

// 防止XSS攻击的安全工具函数
const XSSProtection = {
// 输入验证和清理
sanitizeInput(input, options = {}) {
if (typeof input !== 'string') {
return input;
}

const {
allowBasicHtml = false,
allowNumbers = true,
allowWhitespace = true
} = options;

let sanitized = input;

// 基本的HTML标签和属性清理
if (!allowBasicHtml) {
// 使用DOMParser进行更安全的HTML清理
sanitized = this.stripHtmlTags(sanitized);
} else {
// 允许基本HTML但清理危险标签和属性
sanitized = this.sanitizeLimitedHtml(sanitized);
}

// 清理JavaScript代码
sanitized = this.removeJavaScript(sanitized);

// 清理特殊字符
sanitized = this.replaceDangerousCharacters(sanitized, allowNumbers, allowWhitespace);

return sanitized;
},

// 完全剥离HTML标签
stripHtmlTags(input) {
const temp = document.createElement('div');
temp.textContent = input;
return temp.textContent || temp.innerText || '';
},

// 清理有限的HTML内容
sanitizeLimitedHtml(input) {
const allowedTags = ['b', 'i', 'u', 'strong', 'em', 'br', 'p', 'span', 'div'];
const allowedAttributes = ['class', 'id', 'title'];

const parser = new DOMParser();
const doc = parser.parseFromString(input, 'text/html');

// 递归清理节点
const cleanNode = (node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 检查是否为允许的标签
if (!allowedTags.includes(node.tagName.toLowerCase())) {
const text = document.createTextNode(node.textContent);
node.parentNode.replaceChild(text, node);
return text;
}

// 清理属性
Array.from(node.attributes).forEach(attr => {
if (!allowedAttributes.includes(attr.name.toLowerCase()) ||
attr.value.toLowerCase().includes('script:') ||
attr.value.toLowerCase().includes('javascript:')) {
node.removeAttribute(attr.name);
}
});

// 递归清理子节点
Array.from(node.childNodes).forEach(child => cleanNode(child));
}
return node;
};

// 清理body中的所有节点
Array.from(doc.body.childNodes).forEach(child => cleanNode(child));

return doc.body.innerHTML;
},

// 移除JavaScript代码
removeJavaScript(input) {
// 移除script标签
let cleaned = input.replace(/<script[^>]*>.*?<\/script>/gi, '');
// 移除事件处理器
cleaned = cleaned.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '');
// 移除javascript:协议
cleaned = cleaned.replace(/javascript:/gi, '');
// 移除vbscript:协议
cleaned = cleaned.replace(/vbscript:/gi, '');

return cleaned;
},

// 替换危险字符
replaceDangerousCharacters(input, allowNumbers, allowWhitespace) {
let cleaned = input;

// 替换引号和反引号
cleaned = cleaned.replace(/["'`]/g, '');

// 替换尖括号
cleaned = cleaned.replace(/[<>]/g, '');

// 替换反斜杠
cleaned = cleaned.replace(/\\/g, '');

// 如果不允许数字,移除所有数字
if (!allowNumbers) {
cleaned = cleaned.replace(/\d/g, '');
}

// 如果不允许空白字符,移除所有空白
if (!allowWhitespace) {
cleaned = cleaned.replace(/\s/g, '');
}

return cleaned;
},

// 安全地设置HTML内容
setSafeInnerHTML(element, html, options = {}) {
if (!element || !(element instanceof HTMLElement)) {
console.error('无效的DOM元素');
return false;
}

try {
// 清理HTML
const sanitizedHtml = this.sanitizeInput(html, {
allowBasicHtml: options.allowBasicHtml || false
});

// 使用DOMPurify库(如果可用)进行更高级的清理
if (window.DOMPurify) {
const purifiedHtml = DOMPurify.sanitize(sanitizedHtml, {
ALLOWED_TAGS: options.allowedTags || [],
ALLOWED_ATTR: options.allowedAttributes || []
});
element.innerHTML = purifiedHtml;
} else {
// 备用方案:使用textContent或innerText
if (options.allowBasicHtml) {
element.innerHTML = sanitizedHtml;
} else {
element.textContent = sanitizedHtml;
}
}

return true;
} catch (error) {
console.error('设置安全HTML内容时出错:', error);
// 出错时使用最安全的方式
element.textContent = input;
return false;
}
},

// 安全地获取元素内容
getSafeElementContent(element, asHtml = false) {
if (!element || !(element instanceof HTMLElement)) {
return '';
}

if (asHtml) {
// 获取HTML内容并清理
const html = element.innerHTML;
return this.sanitizeInput(html, { allowBasicHtml: true });
} else {
// 获取纯文本内容
return element.textContent || element.innerText || '';
}
},

// 检查输入是否包含XSS攻击向量
containsXSSVectors(input) {
if (typeof input !== 'string') {
return false;
}

// 常见的XSS攻击向量模式
const xssPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/vbscript:/gi,
/on\w+\s*=\s*["'][^"']*["']/gi,
/expression\s*\(/gi,
/data:text\/html/gi,
/<\s*img[^>]*src\s*=/gi,
/<\s*svg[^>]*onload\s*=/gi
];

// 检查是否匹配任何XSS模式
return xssPatterns.some(pattern => pattern.test(input));
},

// 配置内容安全策略(CSP)
configureCSP(nonce) {
const cspHeader = `default-src 'self';\n` +
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic';\n` +
`style-src 'self' 'unsafe-inline';\n` +
`img-src 'self' data: https:;\n` +
`connect-src 'self' https://api.yoursite.com https://*.infura.io;\n` +
`frame-src 'none';\n` +
`object-src 'none';\n` +
`base-uri 'self';\n` +
`form-action 'self';\n` +
`report-uri /csp-report-endpoint;`;

return cspHeader;
},

// 生成CSP nonce值
generateCSPNonce() {
const array = new Uint8Array(16);
window.crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
};

// 使用示例:安全处理用户输入
function handleUserInput(inputElement) {
const rawInput = inputElement.value;

// 检查是否包含XSS攻击向量
if (XSSProtection.containsXSSVectors(rawInput)) {
console.warn('检测到潜在的XSS攻击向量');
showXSSWarning();
// 可以选择阻止提交或自动清理
return false;
}

// 清理用户输入
const sanitizedInput = XSSProtection.sanitizeInput(rawInput, {
allowBasicHtml: false, // 不允许HTML
allowNumbers: true,
allowWhitespace: true
});

console.log('清理后的输入:', sanitizedInput);

// 安全地显示用户输入
const outputElement = document.getElementById('output-area');
XSSProtection.setSafeInnerHTML(outputElement, sanitizedInput);

return true;
}

// 配置内容安全策略(CSP)
function setupContentSecurityPolicy() {
const nonce = XSSProtection.generateCSPNonce();
const cspHeader = XSSProtection.configureCSP(nonce);

// 设置CSP元标签
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = cspHeader;
document.head.appendChild(meta);

console.log('内容安全策略已配置');

// 在需要执行内联脚本时使用nonce
return nonce;
}

// 应用启动时配置安全策略
const cspNonce = setupContentSecurityPolicy();

点击劫持(Clickjacking)攻击

点击劫持攻击通过覆盖透明层诱导用户点击实际上是恶意的按钮或链接。

// 防止点击劫持的安全措施
const ClickjackingProtection = {
// 配置X-Frame-Options响应头
configureXFrameOptions() {
// 在服务器端设置X-Frame-Options响应头为DENY或SAMEORIGIN
// 以下代码在前端通过meta标签设置(注意:部分浏览器可能不支持)
const meta = document.createElement('meta');
meta.httpEquiv = 'X-Frame-Options';
meta.content = 'DENY'; // 或 'SAMEORIGIN'
document.head.appendChild(meta);

console.log('X-Frame-Options已设置为DENY');
},

// 设置Content-Security-Policy的frame-ancestors指令
configureFrameAncestors() {
// 在服务器端设置CSP响应头中的frame-ancestors指令
// 以下代码在前端通过meta标签设置
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = 'frame-ancestors none;'; // 不允许任何网站嵌入
// 或 'frame-ancestors \'self\' https://trusted-site.com;'
document.head.appendChild(meta);

console.log('CSP frame-ancestors已设置');
},

// 实现JavaScript帧破坏代码
implementFrameBuster() {
try {
// 检测是否在iframe中
if (window.self !== window.top) {
// 如果在iframe中,重定向到顶层窗口
window.top.location.href = window.location.href;
console.warn('检测到点击劫持尝试,已执行帧破坏');
return true;
}

// 监听窗口大小变化,防止被缩小到透明框架中
window.addEventListener('resize', this.detectFrameResizing);

// 监听鼠标位置,检测异常点击模式
document.addEventListener('mousedown', this.detectSuspiciousClicks);

return false;
} catch (error) {
console.error('设置帧破坏代码时出错:', error);
// 出错时可能表示被iframe嵌入且不允许修改顶层窗口
showClickjackingWarning();
return true;
}
},

// 检测窗口大小调整(可能是点击劫持的迹象)
detectFrameResizing() {
const threshold = 100; // 像素阈值
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

// 检查窗口是否被过度缩小
if (viewportWidth < threshold || viewportHeight < threshold) {
console.warn('检测到异常的窗口大小调整,可能是点击劫持尝试');
showClickjackingWarning();
}
},

// 检测可疑点击模式
detectSuspiciousClicks(event) {
const clickDelayThreshold = 150; // 毫秒
const lastClickTime = ClickjackingProtection.lastClickTime || 0;
const currentTime = Date.now();
const clickDelay = currentTime - lastClickTime;

// 记录当前点击时间
ClickjackingProtection.lastClickTime = currentTime;

// 检查点击速度是否异常快
if (clickDelay > 0 && clickDelay < clickDelayThreshold) {
console.warn('检测到异常快速的点击模式,可能是自动化点击或点击劫持');
// 可以选择记录此行为或显示警告
}

// 检查点击位置是否总是精确在特定元素上(可能是被覆盖的按钮)
const clickedElement = event.target;
ClickjackingProtection.trackClickPosition(clickedElement, event.clientX, event.clientY);
},

// 跟踪点击位置模式
trackClickPosition(element, x, y) {
// 简化实现,记录点击位置模式以便分析
const clickData = {
element: element.tagName,
id: element.id,
className: element.className,
x: x,
y: y,
timestamp: Date.now()
};

// 存储最近的点击数据(最多10条)
if (!ClickjackingProtection.recentClicks) {
ClickjackingProtection.recentClicks = [];
}

ClickjackingProtection.recentClicks.push(clickData);

if (ClickjackingProtection.recentClicks.length > 10) {
ClickjackingProtection.recentClicks.shift();
}
},

// 为关键按钮添加防点击劫持保护
protectCriticalButtons(buttonSelectors) {
buttonSelectors.forEach(selector => {
const buttons = document.querySelectorAll(selector);

buttons.forEach(button => {
// 添加事件监听器检测异常点击
button.addEventListener('click', function(event) {
const isSafe = ClickjackingProtection.verifyClickSafety(event);

if (!isSafe) {
event.preventDefault();
event.stopPropagation();
console.warn('阻止了可疑点击');
showClickjackingWarning();
}
});

// 添加视觉指示器(可选)
const indicator = document.createElement('span');
indicator.className = 'click-safety-indicator';
indicator.style.display = 'none';
button.appendChild(indicator);
});
});
},

// 验证点击安全性
verifyClickSafety(event) {
const clickedElement = event.target;

// 检查点击目标是否是预期的元素
const isExpectedElement = clickedElement.tagName.toLowerCase() === 'button' ||
clickedElement.tagName.toLowerCase() === 'a';

// 检查点击事件是否有异常属性
const hasSuspiciousProperties = event.isTrusted === false ||
event.detail < 0 ||
event.screenX < 0 ||
event.screenY < 0;

// 检查元素是否可见
const isElementVisible = this.isElementFullyVisible(clickedElement);

// 综合判断
const isSafe = isExpectedElement && !hasSuspiciousProperties && isElementVisible;

return isSafe;
},

// 检查元素是否完全可见
isElementFullyVisible(element) {
const rect = element.getBoundingClientRect();
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;

// 检查元素是否完全在视口内
const isInViewport = rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= viewportHeight &&
rect.right <= viewportWidth;

// 检查元素是否被其他元素覆盖
const isNotCovered = this.checkIfElementIsCovered(element);

return isInViewport && isNotCovered;
},

// 检查元素是否被其他元素覆盖
checkIfElementIsCovered(element) {
// 简化实现:检查元素的z-index和opacity
const style = window.getComputedStyle(element);
const zIndex = parseInt(style.zIndex) || 0;
const opacity = parseFloat(style.opacity);

// 检查元素是否可见
if (opacity === 0 || style.display === 'none' || style.visibility === 'hidden') {
return false;
}

// 检查是否有更高z-index的元素覆盖在上面
// 注意:这个实现不完整,实际应用中可能需要更复杂的检测
// 例如,使用document.elementFromPoint来检测点击位置的顶层元素

return true;
}
};

// 使用示例:应用点击劫持防护
function applyClickjackingProtection() {
// 配置X-Frame-Options
ClickjackingProtection.configureXFrameOptions();

// 配置CSP frame-ancestors
ClickjackingProtection.configureFrameAncestors();

// 实现帧破坏代码
const inFrame = ClickjackingProtection.implementFrameBuster();

if (inFrame) {
console.warn('应用可能被嵌入在iframe中,存在点击劫持风险');
// 显示警告或采取其他措施
showClickjackingWarning();
}

// 保护关键按钮
ClickjackingProtection.protectCriticalButtons(['.wallet-connect-button', '.transaction-confirm-button', '.approve-button']);
}

// 在页面加载时应用点击劫持防护
window.addEventListener('load', applyClickjackingProtection);

DNS劫持与中间人攻击

DNS劫持和中间人攻击会拦截并修改前端与区块链节点之间的通信,导致交易被篡改。

// 防止DNS劫持和中间人攻击的安全措施
const DNSSecurityProtection = {
// 验证网站域名和证书
validateDomainAndCertificate() {
try {
// 检查是否使用HTTPS
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
console.warn('警告:网站未使用HTTPS协议,存在安全风险');
showInsecureConnectionWarning();
return false;
}

// 检查证书有效性(现代浏览器会自动处理,但可以添加额外验证)
// 注意:JavaScript无法直接访问证书详细信息,除非使用特殊API

// 验证域名是否符合预期
const expectedDomains = ['your-dapp.com', 'www.your-dapp.com', 'test.your-dapp.com'];
const currentDomain = window.location.hostname;

if (!expectedDomains.includes(currentDomain)) {
console.warn('警告:当前域名不在预期列表中,可能存在DNS劫持风险');
showDomainMismatchWarning(currentDomain);
return false;
}

return true;
} catch (error) {
console.error('验证域名和证书时出错:', error);
return false;
}
},

// 使用DNS over HTTPS (DoH)解析域名
configureDNSOverHTTPS() {
// 注意:这需要在浏览器设置或通过特殊的API实现
// 前端JavaScript无法直接配置浏览器的DNS解析方式

// 但可以检测浏览器是否支持DoH
const supportsDoH = this.detectDoHSupport();

if (!supportsDoH) {
console.warn('浏览器不支持DNS over HTTPS,建议用户切换到支持DoH的浏览器');
showDoHNotSupportedWarning();
}

return supportsDoH;
},

// 检测浏览器是否支持DoH
detectDoHSupport() {
// 简化实现:检查浏览器类型和版本
const userAgent = navigator.userAgent.toLowerCase();

// Chrome 83+、Firefox 62+、Edge 83+等支持DoH
const isChrome = /chrome/.test(userAgent) && !/edge/.test(userAgent);
const isFirefox = /firefox/.test(userAgent);
const isEdge = /edge/.test(userAgent);

// 提取浏览器版本号
let version = 0;
if (isChrome) {
version = parseInt(userAgent.match(/chrome\/([0-9]+)/)[1]);
} else if (isFirefox) {
version = parseInt(userAgent.match(/firefox\/([0-9]+)/)[1]);
} else if (isEdge) {
version = parseInt(userAgent.match(/edge\/([0-9]+)/)[1]);
}

// 判断是否支持DoH
return (isChrome && version >= 83) ||
(isFirefox && version >= 62) ||
(isEdge && version >= 83);
},

// 验证API端点TLS证书
async verifyAPITLSCertificate(apiUrl) {
try {
// 注意:前端JavaScript无法直接访问TLS证书详细信息
// 此函数仅作为示例,实际应用中可能需要使用特殊API或服务

// 尝试与API建立连接并检查响应
const response = await fetch(apiUrl, {
method: 'HEAD',
mode: 'cors',
credentials: 'omit'
});

if (!response.ok) {
console.error('API连接失败:', response.status);
return false;
}

// 检查响应头中的安全相关信息
const strictTransportSecurity = response.headers.get('strict-transport-security');
if (!strictTransportSecurity) {
console.warn('API未设置HSTS头部,建议启用');
}

// 验证响应是否来自预期的源
const responseUrl = new URL(response.url);
const expectedHostnames = ['api.your-dapp.com', 'infura.io', 'alchemyapi.io'];

if (!expectedHostnames.some(hn => responseUrl.hostname.includes(hn))) {
console.warn('API响应来自意外的主机名,可能存在中间人攻击');
return false;
}

return true;
} catch (error) {
console.error('验证API TLS证书时出错:', error);
return false;
}
},

// 使用固定IP地址和证书指纹
setupPinning(pinnedHosts) {
// HTTP Public Key Pinning (HPKP)已被大多数浏览器弃用
// 现代替代方案是Certificate Transparency和Expect-CT头部

// 设置Expect-CT头部(服务器端配置更安全)
const meta = document.createElement('meta');
meta.httpEquiv = 'Expect-CT';
meta.content = 'max-age=86400, enforce, report-uri="https://your-report-uri.com/report"';
document.head.appendChild(meta);

console.log('Expect-CT头部已设置');

// 对于API连接,可以实现证书指纹固定
return {
verifyCertificateFingerprint: async (apiUrl, expectedFingerprint) => {
// 注意:前端JavaScript无法直接获取证书指纹
// 此函数仅作为示例接口
return true;
}
};
},

// 检测和防范中间人攻击
detectMITMAttack() {
// 检测连接异常延迟
this.monitorConnectionLatency();

// 检测响应内容变化
this.monitorContentIntegrity();

// 监听网络事件异常
this.listenForNetworkAnomalies();
},

// 监控连接延迟
monitorConnectionLatency() {
// 定期测量到关键API的连接延迟
const apiUrls = ['https://api.your-dapp.com/health', 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'];
const baselineLatencies = new Map();

async function measureLatency(url) {
try {
const startTime = performance.now();
const response = await fetch(url, { method: 'HEAD' });
const endTime = performance.now();

if (response.ok) {
return endTime - startTime;
}
return -1;
} catch (error) {
console.error('测量延迟时出错:', error);
return -1;
}
}

// 初始测量(建立基线)
async function establishBaseline() {
for (const url of apiUrls) {
const latency = await measureLatency(url);
if (latency > 0) {
baselineLatencies.set(url, latency);
}
}
}

// 定期检查
async function checkLatency() {
for (const url of apiUrls) {
const baseline = baselineLatencies.get(url);
if (baseline) {
const current = await measureLatency(url);

if (current > 0) {
const increaseRatio = (current - baseline) / baseline;

// 如果延迟增加超过200%,可能存在中间人攻击
if (increaseRatio > 2.0) {
console.warn(`检测到到${url}的连接延迟异常增加,可能存在中间人攻击`);
showConnectionAnomalyWarning(url);
}
}
}
}
}

// 建立基线并开始定期检查
establishBaseline().then(() => {
// 每5分钟检查一次
setInterval(checkLatency, 5 * 60 * 1000);
});
},

// 监控内容完整性
monitorContentIntegrity() {
// 为关键静态资源添加SRI (Subresource Integrity)
// 示例:<script src="https://example.com/script.js" integrity="sha384-..." crossorigin="anonymous"></script>

// 检测页面内容是否被篡改
const criticalElements = ['.wallet-connect-button', '#transaction-amount', '.contract-address'];

// 存储关键元素的初始状态哈希
const elementHashes = new Map();

// 生成内容的简单哈希
function generateContentHash(content) {
let hash = 0;
if (content.length === 0) return hash;

for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}

return hash.toString(16);
}

// 建立初始哈希
function establishElementHashes() {
criticalElements.forEach(selector => {
const elements = document.querySelectorAll(selector);

elements.forEach((element, index) => {
const key = `${selector}_${index}`;
const content = element.textContent || element.value || '';
const hash = generateContentHash(content);

elementHashes.set(key, hash);
});
});
}

// 检查内容是否被篡改
function checkContentIntegrity() {
criticalElements.forEach(selector => {
const elements = document.querySelectorAll(selector);

elements.forEach((element, index) => {
const key = `${selector}_${index}`;
const originalHash = elementHashes.get(key);

if (originalHash) {
const currentContent = element.textContent || element.value || '';
const currentHash = generateContentHash(currentContent);

if (currentHash !== originalHash) {
console.warn(`检测到元素${selector}内容被篡改,可能存在中间人攻击`);
showContentTamperingWarning(selector);
// 恢复原始内容(如果可能)
restoreOriginalContent(element, key);
}
}
});
});
}

// 恢复原始内容(简化实现)
function restoreOriginalContent(element, key) {
// 注意:在实际应用中,这可能需要从可信来源重新加载内容
console.log(`尝试恢复元素${key}的原始内容`);
}

// 建立初始哈希并开始定期检查
establishElementHashes();
setInterval(checkContentIntegrity, 60 * 1000); // 每分钟检查一次
},

// 监听网络异常
listenForNetworkAnomalies() {
// 监听网络连接变化
window.addEventListener('online', () => {
console.log('网络已连接');
// 连接恢复后验证关键资源
verifyCriticalResources();
});

window.addEventListener('offline', () => {
console.warn('网络连接已断开');
});

// 监听DNS解析错误
// 注意:JavaScript无法直接监听DNS解析错误,但可以检测请求失败模式

// 监控XMLHttpRequest和fetch请求的失败模式
this.monitorRequestFailures();
},

// 监控请求失败模式
monitorRequestFailures() {
const failureThreshold = 3; // 连续失败阈值
let consecutiveFailures = 0;

// 重写fetch API来监控请求失败
const originalFetch = window.fetch;

window.fetch = async function(url, options) {
try {
const response = await originalFetch.apply(this, arguments);

if (!response.ok) {
console.warn(`请求${url}失败,状态码: ${response.status}`);
consecutiveFailures++;
} else {
consecutiveFailures = 0;
}

// 检查连续失败次数
if (consecutiveFailures >= failureThreshold) {
console.warn('检测到连续请求失败,可能存在网络问题或中间人攻击');
showNetworkFailureWarning();
}

return response;
} catch (error) {
console.error(`请求${url}抛出异常:`, error);
consecutiveFailures++;

// 检查连续失败次数
if (consecutiveFailures >= failureThreshold) {
console.warn('检测到连续请求失败,可能存在网络问题或中间人攻击');
showNetworkFailureWarning();
}

throw error;
}
};
},

// 验证关键资源
async verifyCriticalResources() {
const criticalResources = [
'/js/wallet-connector.js',
'/js/contract-interaction.js',
'/css/main.css'
];

for (const resource of criticalResources) {
try {
const response = await fetch(resource, { method: 'HEAD' });

if (!response.ok) {
console.error(`关键资源${resource}验证失败`);
// 可以尝试从备用CDN加载
await loadFromBackupCDN(resource);
}
} catch (error) {
console.error(`验证关键资源${resource}时出错:`, error);
// 可以尝试从备用CDN加载
await loadFromBackupCDN(resource);
}
}
}
};

// 使用示例:应用DNS和中间人攻击防护
function applyDNSSecurityProtection() {
// 验证域名和证书
const isDomainValid = DNSSecurityProtection.validateDomainAndCertificate();

if (!isDomainValid) {
console.error('域名验证失败,应用可能处于不安全的环境中');
showCriticalSecurityWarning();
}

// 配置DNS over HTTPS
DNSSecurityProtection.configureDNSOverHTTPS();

// 设置证书固定
const pinner = DNSSecurityProtection.setupPinning({
'api.your-dapp.com': 'sha256/...', // 证书指纹
'infura.io': 'sha256/...'
});

// 检测和防范中间人攻击
DNSSecurityProtection.detectMITMAttack();

// 定期验证API端点TLS证书
setInterval(async () => {
const apiUrl = 'https://api.your-dapp.com/v1/endpoint';
const isValid = await DNSSecurityProtection.verifyAPITLSCertificate(apiUrl);

if (!isValid) {
console.error('API TLS证书验证失败,可能存在中间人攻击');
showAPICertificateWarning(apiUrl);
}
}, 10 * 60 * 1000); // 每10分钟验证一次
}

// 在页面加载时应用DNS安全防护
window.addEventListener('load', applyDNSSecurityProtection);