
本文是 Consensys Diligence 与 MSQ 项目合作的成果,并获得了 DFinity 基金会和 MetaMask Snaps DAO 的支持。

Consensys Diligence 是 Consensys Software Inc. 旗下的审计部门,以其在 Solidity、Cairo 及各类 ZK 库审计领域无与伦比的专业能力享誉业界。Diligence 团队正引领 MetaMask Snaps 的审计与安全标准建设,并与 MetaMask 团队紧密协作,共同保障钱包功能的安全性。此外,Diligence 团队还积极探索安全工具开发,以及在最严苛生产环境中经过验证的前沿持续模糊测试解决方案。
MSQ 项目近期已在 MetaMask Snaps 商店正式上线。MSQ 是一款 Snap,允许用户与 Internet Computer 网络交互、管理基于 IC 的代币、在基于 IC 的 dapp 中安全授权,以及支付数字商品和服务。本质上,它是一款集成于 MetaMask 的加密钱包,专为 Internet Computer 设计,具备独特的授权与支付功能。
Diligence 与 MSQ 团队共同确定了本文涵盖的五个核心领域——文档、软件设计、代码、供应链和修复——列举了 MSQ 团队在开发过程中采用的实践方法,并附有 Diligence 团队对这些实践安全价值的深度解读,为任何正在构建 MetaMask 钱包 Snap 或其他可能接受安全审计的关键系统的开发者提供实用参考。让我们开始吧!
文档能将你脑海中的概念转化为他人可见的内容。分层文档通过逐步引导读者进入语境,帮助他们按照你的意图理解这些概念。显然,文档越清晰,就能为项目提供越宏观的视角,他人也能更好地理解你的意图。
我们以读者通常访问文档的逆序来定义各文档层级,每一层都提供更高层次的抽象。这样,从宏观视角出发的读者可以逐步深入细节。
关于软件最权威的信息来源是代码本身,它构成了文档的基础层。虽然代码包含所有必要信息,但处理这些信息并不容易。代码越简洁,从中提炼高层抽象就越容易。但在真实系统中,这些抽象往往隐藏在系统组件的复杂交互、数周积累的技术债务以及临时解决方案之后。
因此,首先要保护阅读代码的人不被系统中某些复杂部分所误导。最佳方式是降低阅读代码所需的认知负担。一些可行策略包括:
提升代码可导航性,便于在 IDE 中轻松跳转。
使用详尽的命名,清晰表达代码元素的用途和功能。
采用强类型语言,明确定义数据格式。
MSQ Snap 和 dapp 前端均使用 TypeScript 开发,有效规避了大量潜在错误,避免了纯 JavaScript 的诸多陷阱。同样,我们的后端采用 Rust,以其强大的类型系统著称。此外,MSQ Snap 导出了一系列可由第三方应用调用的方法。但我们不使用字符串来标识这些方法,而是使用常量。我们定义了如下常量对象:
在代码中,这个常量对象可以作为访问正确方法的指引,读者通过对象中的标识符就能了解该方法的大量信息。例如,方法 SNAP_METHODS.protected.identity.login 只能由 MSQ 的配套 dapp 调用,用于允许用户登录。这种方式提升了代码清晰度,也简化了重构工作,因为方法名称的变更可以集中管理,而无需在多处逐一修改。
为了匹配方法名称,我们使用了一个包含如下分支的大型 switch 表达式:
如你所见,阅读时几乎不可能产生误解——所有内容命名精确,没有任何歧义空间。
Diligence 点评 时间是关键因素。开发者有数周时间来理解代码库,而安全工程师往往只有数天来识别风险。这一限制凸显了降低代码认知负担的重要性——这一实践对开发效率和安全性都大有裨益。
从 MSQ 代码库中我们可以学到:清晰、可导航的代码,配合描述性命名和强类型(如 TypeScript),能有效减少误解——而误解正是漏洞的常见根源。
但请记住,TypeScript 类型注解仅在编译时检查!对于运行时处理或解析的数据(RPC、JSON 等),还需要额外的类型强制执行、数据格式和边界检查!为此,MSQ 优雅地采用了 zod——一个以 TypeScript 为优先、支持静态类型推断的 schema 验证库,我们建议各团队效仿这一做法。
MSQ 在代码注释量上拿捏得恰到好处,通过适量注释清晰表达代码意图。这种清晰度帮助安全研究人员快速把握代码库的"主线"和核心概念。编写易于理解的代码,能显著提升快速安全审查的效果。
我们认为注释的目的并非覆盖 100% 的代码,而是对复杂或写得不够清晰的部分进行标注。正则表达式、密码学和字符串操作等领域尤其需要清晰的注释。
例如,MSQ 中有一个函数,用于转义字符串中可能存在的 Markdown 控制字符。其函数体只有一行代码,但涉及正则表达式匹配,因此我们在注释中描述了其功能:
如果你不排斥在工作流中使用 AI,ChatGPT 非常擅长快速为此类工具函数生成优质注释(只需仔细核对其输出即可)。
Diligence 点评 作为安全审查人员,我们将函数注释视为穿越陌生代码的路线图。请记住,我们审查代码的时间有限,当注释与实现不符时,这不仅令人困惑——更是一个危险信号。
这往往意味着函数或其描述偏离了规范,预示着潜在漏洞。也许某个函数跳过了承诺的检查,或以意外方式处理数据,这可能导致严重后果。
因此,请保持注释简洁准确,就像 MSQ 在其代码库中所做的那样。内联注释不只是备注,它们是引导我们穿越代码的灯塔,而注释与实现的不一致则是某处可能存在问题的线索。
在确保代码及其复杂性得到充分记录之后,下一步是梳理系统架构。这包括详细描述组件间的交互,以及软件所采用的协议、算法和数据结构。这一层主要使用人类可读的格式,如文本和图表,而非代码。
MSQ 的系统文档包括架构概览和集成指南。这些文档内容丰富,包含文字描述、示例和图表,清晰阐明了不同系统部分的交互方式。
时序图对于可视化组件交互极具价值。我们推荐使用 diagrams.net(draw.io),这是一款免费工具,已在各类项目中证明了其多功能性。
Diligence 点评 与详述函数的内联注释不同,系统文档提供了一个万米高空视角,呈现应用的整体全貌。这一视角对于发现跨组件问题或用户流程漏洞至关重要,而这些问题在代码层面往往难以察觉。
可以把安全审查想象成一场《命令与征服》游戏。你从一张大部分被遮蔽的地图开始。随着探索推进,系统文档就像侦察单位,逐步揭开完整架构——权限、Snap 与 dApp 的交互、用户交互。这种逐步展开的全局视图让审查人员能够追踪用户旅程,就像追踪游戏单位的移动,从而发现可能绕过安全机制的路径。
此外,完善的系统文档应包含安全相关备注,理想情况下还应包含威胁建模练习的输出结果,清晰概述主要组件、交互关系、风险、参与者和典型用户流程。这有助于审查人员聚焦高优先级领域,发现与规范的偏差。
对于开发者而言,良好的系统文档不只是文案工作,它是一张共享地图,指引攻防策略,将安全审查从盲目探索转变为有的放矢的战略行动。
这一层涵盖多种形式的材料,如文章、落地页、博客文章、演示视频等。
这些信息主要涉及业务层面,但它们能很好地回答"是什么?"这一问题,而前面所有层级都在以不同详细程度回答"如何做?"这一问题。
新用户最有可能通过这类材料初次接触你的项目,因此尽量保持这一层简洁直观是个好主意。此外,这类材料通常是唯一能展示软件实际运行效果的内容——用户如何借助你的软件从状态 A 到达状态 B。通过亲眼看到软件的运行方式,人们能了解软件的核心理念,进而对其底层工作原理做出合理推测。
例如,MSQ 落地页的主屏幕以大字体写道:"USE THE INTERNET COMPUTER WITH METAMASK"。读完这句简单的话,读者大概会猜到:"啊,这是一个可以存储 Internet Computer 区块链代币的钱包 Snap。它的工作方式可能与其他区块链的 Snap 类似,只是使用了 IC 开发生态系统的技术"——这个猜测完全正确。
Diligence 点评 我们在审查 MSQ 及众多其他区块链项目的经验中,始终印证了一点:清晰的高层文档不仅有益,对于用户采用和安全性而言都至关重要。在审查 MSQ 的 Snap 时,他们的多层文档方式令我们眼前一亮。开发者提供的视频演示让我们迅速上手,甚至在查看代码之前就能理解 Snap 的功能。这个高层概览展示了用户界面,并附有对预期行为和典型用户流程的说明。这类可视化辅助材料极具价值,能让安全专家更快速地把握应用中编码的预期"正常路径",并寻找那些往往隐藏在这些路径之外的漏洞。这与 Snap 和代码的实时演示相结合,通常是安全审查的理想开端。
这部分可能是项目安全性中最重要的内容。你的代码可以做到百分之百无 bug,基础设施可以用价值数百万美元的各种措施加以保护。但如果你的设计存在某些根本性缺陷,可被恶意行为者利用,那么所有这些努力都将付诸东流。
虽然具体示例因使用场景而异,我们提供几条通用建议。
在 MSQ 项目伊始,我们确立了与愿景相符的核心安全原则:
MSQ 团队对用户信息一无所知:我们不收集任何用户敏感信息,包括密钥材料、授权会话数据或账户详情。
加密材料永不离开 Snap:受保护的用户 MetaMask 熵值仅在 Snap 内部处理,按需生成,使用后立即销毁。
MSQ 具有确定性:用户无需任何额外知识,仅凭根助记词即可确定性地恢复所有资金、身份和密钥对。
MSQ 以隐私为先:我们将用户隐私置于一切之上。
这些原则是针对 MSQ 的具体需求量身定制的。此外,MetaMask 团队也为 Snap 开发者提供了一套安全准则,MSQ 严格遵守,详情请见此处。
例如,最小权限原则意味着你的 Snap 向用户申请的权限越少,安全性越高。MSQ 仅申请 4 项权限:
endowment:rpc
snap_dialog
snap_manageState
snap_getEntropy
我们不需要 endowment: network access,因为 MSQ 只负责签署交易,而非将其传递到 IC——这一职责由集成的 dapp 承担。
你在构建 Snap 时也应如此——以 MetaMask 的默认准则为基础,结合自身具体规则加以扩展,并严格遵守。
Diligence 点评 我们常对客户说:"安全不是你添加的功能,而是你围绕其设计的原则。"MSQ 在项目伊始就确立核心安全原则,正是我们所期望看到的。太多项目将安全视为事后补救,在核心架构确定后才附加安全措施。这种补丁式做法不可避免地留下代码层面修复无法真正解决的根本性缺陷。
在 Consensys Diligence,我们提供安全专家服务和工具,并倡导维护终端用户的权益。这包括确保用户密钥材料安全保存在钱包信任边界内,不会无意间暴露在该范围之外;保护用户隐私,理想情况下防止元数据或侧信道信息泄露给第三方;以及推动 Snap 仅申请功能所需的最小权限——即最小权限原则。
MSQ 的主动安全姿态与这些原则高度契合。从一开始就将安全置于优先位置,他们正在构建一个更坚实的基础——天然对漏洞具有更强的抵抗力。
这更像是一条元准则——它是任何软件都应遵循的原则:从网络游戏到银行系统,概莫能外。
MSQ 中最典型的例子是我们的身份管理方式。众所周知,大多数钱包会为用户提供一组身份(地址、账户等),经用户同意后,任何 dapp 均可访问这组身份,例如用于签署交易。
这对 dapp 之间的集成非常有用,因为任何 dapp 都可以代表用户调用其他 dapp 的智能合约方法,从而相互扩展能力。例如,一个加密支付平台可以调用 Uniswap 的智能合约,自动将买家的代币兑换为卖家期望收到的支付币种。
但这对安全有害,因为诈骗网站可能诱骗用户签署恶意交易,清空其钱包。
与此相对,MSQ 采用了身份隔离技术——每个 dapp(每个不同的 URL 来源)都有其独立的用户身份集合,无法与其他 dapp 的用户身份交互。这不仅解决了诈骗网站问题,还极大地保护了隐私,因为 dapp 无法再追踪用户,也无法查看用户与哪些智能合约(在 IC 中称为 canister)进行了交互。这一方法受 Internet Identity 和 WebAuthn 规范启发,显著降低了安全风险。
例如,如果一个知名 dapp 遭到攻击,传统钱包将在所有 dapp 上都面临风险。相比之下,MSQ 的身份隔离将威胁限制在被攻击的 dapp 范围内,防止损害扩散。
然而,这种方法限制了 dapp 之间的直接集成,因为用户在每个 dapp 上拥有不同的身份。MSQ 针对特定场景提供了解决方案,例如允许在双方同意的情况下在 dapp 之间共享身份,适用于域名变更等情形。
此外,MSQ 确保 dapp 可访问的数据严格与相关身份绑定,进一步保护用户信息,并在不同应用间维护隐私。
Diligence 点评 限制 dApp 直接集成的权衡是经过深思熟虑的。在我们的风险评估中,我们经常讨论"安全预算"——承认完美的安全性可能会影响功能性。MSQ 将这一预算用得恰到好处。通过限制跨 dApp 交互,他们大幅降低了最严重的风险之一(大规模身份滥用),同时保留了核心钱包功能。
在讨论文档时,我们已经简要谈到了代码。这里我们想强调代码的另外几个方面,这些方面更多与技术相关,而非可读性。
这里的"健壮"是指代码行为可预测,能处理所有预期和非预期的输入。例如,当你的 Snap 期望接收 hex 格式时,如果收到 base64 字符串应如何响应?如果方法被以非预期顺序调用,或调用了未定义的 RPC 方法,又该如何处理?
解决这些不确定性需要:
清晰的错误处理: 确保代码中的每个分支和情况都经过检查并得到妥善处理。在 MSQ 的 Snap 中,我们在 switch-case 语句中使用 TypeScript 的穷举检查,以捕获任何意外的方法调用,如下所示:
此设置可捕获使用未知 RPC 方法标识符的情况。
输入净化: 根据预期类型和格式验证输入数据可能较为复杂,但对安全性至关重要。我们在 TypeScript 中使用 zod 库,将 TypeScript 的类型系统扩展到运行时检查。Zod 允许详细的 schema 定义,确保只有有效数据才会被处理。
例如,我们的方法 SNAP_METHODS.protected.identity.login 使用 zod schema 对输入进行严格验证:
这里,ZOrigin 和 ZIdentityId 不仅仅是字符串或数字,而是经过专门验证的子类型,确保输入不仅类型正确,在语境上也合适。
通过尽可能精确地定义 zod schema,你可以预先解决许多潜在问题。审计人员建议在 MSQ 中强制执行严格验证,实施后发现并修复了多个安全风险。
Diligence 点评 我们不止一次听到客户反馈,根据我们针对特定攻击场景的建议实施更严格的验证后,又发现了其他几个安全风险。这与我们的经验完全吻合,这也是我们始终倡导严格输入验证以缩小攻击面的原因。在许多安全审查中,我们首先审查输入验证。收紧这一环节往往会暴露潜伏的问题——修复一个漏洞引发连锁反应,揭示另一个漏洞。这清楚地表明项目认真对待我们的反馈,持续强化其系统。随着攻击面的缩小,攻击者成功利用弱点的难度也在逐步提升。
在 MSQ,我们认为有责任为 dapp 开发者提供一个直观的库,便于他们正确、高效地与我们的 Snap 交互。这不仅让 MSQ,也让第三方开发者能够维护简洁健壮的代码库。
这样的库有以下几方面优势:
简化 API 复杂性: 我们将 Snap 的复杂性抽象为用户友好的 API。例如,MSQ 的客户端库使用 CBOR 进行数据编码,绕过了 JSON-RPC 的限制,免去了开发者手动编解码数据的麻烦。
确保安全直接的交互: 某些 Snap 功能仅限于我们的配套 dapp 使用。为此,我们通过 postMessage 管理重定向和数据传输流程,确保无缝衔接。这种内部处理保证了第三方开发者通过清晰、简洁的 API 与我们的 Snap 交互。
增强语义清晰度: 简洁的 API 不仅是工具,更是一种沟通媒介,使代码更具可读性和可理解性,摆脱了复杂编解码流程和额外操作层的干扰。
通过优先考虑这些方面,MSQ 致力于构建一个生态系统,让我们的 Snap 和集成的 dapp 都能高效、安全地运行。
Diligence 点评 MSQ 通过其库进行第三方代码集成的方式,体现了在简化开发的同时维护安全性的承诺。虽然在 Snap 上下文中必须严格执行安全措施,但面向 dapp 的库可以为与 Snap 通信提供便捷方式。然而,需要始终注意的是,dapp 开发者在 dapp 上下文中使用时,需确保与 Snap 的安全交互和数据的安全编码。
近期的 XZ 后门事件再次强调了防止恶意代码进入依赖项的重要性。MSQ 使用 pnpm 管理依赖,但实际上你偏好哪种包管理器并无本质区别。我们在 MSQ 采取了两项措施来防范此类攻击向量:
使用 pnpm install --save <package-name> 通常会安装最新版本的包,遵循 semver 规范,允许自动更新到不破坏兼容性的新版本。这在 package.json 中以插入符号(^)表示,代表可接受的版本范围:
"zod": "^3.22.4" // Installs version 3.22.4 or newer up to 4.0.0然而,为防范恶意更新带来的风险,将依赖锁定到特定版本更为安全:
从 package.json 中删除插入符号(^),固定每个依赖的版本。删除现有的锁文件和 node_modules,然后执行 pnpm install,严格按照定义的版本安装。
在未来安装时使用 --save-exact 标志(pnpm install --save-exact <package-name>),避免自动更新。
信任程度是这一决策的考量因素。例如,在 MSQ 我们认为 @metamask 和 @dfinity 依赖命名空间是安全的,允许在其 semver 范围内更新。但在这样做时,请注意传递依赖可能带来的潜在漏洞。
Diligence 点评 将项目切换到精确依赖版本已经防止了无数问题:
它阻止了静默更新重新引入已修复的漏洞。
它防止了"版本偏差"——不同模块看到不同的库行为。
它在事后调查中至关重要,使我们能够精确复现当时的环境。每次更新都可能是恶意的,无论是哪个命名空间、哪个包。通过审查所使用的包,并仅绑定到已知安全版本以防止意外更新,可以进一步缩小供应链攻击的攻击面。信任某些供应商是可以的,但前提是在使用前手动验证新版本不含恶意代码。需要注意的是,被众多项目广泛使用的知名包尤其是恶意行为者的攻击目标,历史已经证明,任何此类核心库都可能遭受复杂的供应链攻击(参见 XZ 后门攻击)。
为确保团队一致性并维护可复现的构建,请将锁文件提交到版本控制系统。在 MSQ,我们使用 pnpm workspaces 和 turborepo,需要一个单一的根锁文件 pnpm-lock.yaml。但你的设置可能需要维护多个锁文件;所有锁文件都应纳入版本控制,以保证环境一致性。
Diligence 点评
这一实践有时被视为单纯的开发便利而被忽视,但实际上它是维护安全稳定代码库的关键组成部分。我们多年来审计智能合约、区块链组件,以及现在的 MetaMask Snaps 的经验反复证明,可复现的构建不只是锦上添花,而是基本的安全要求。提交锁文件是缩小供应链攻击面的拼图之一。当 npm install 不再是一场彩票游戏,具体的版本和校验和在仓库中留下审计痕迹,构建更具确定性,"在我机器上能跑"的争论也将成为历史。
虽然这与我们的 Snap 本身没有直接关系,但了解如何高效处理审计发现至关重要。
一旦审计团队报告发现,你就应该立即着手修复。修复的规模和复杂性显然取决于具体发现,但有一条建议可以让修复更小、更简洁——在可能的情况下,修复根本原因,而非表面症状。
例如,在 MSQ,审计人员发现可以在我们的 MetaMask 同意消息中注入 Markdown 控制字符。我们没有逐一修复每条同意消息,在代码的不同位置进行多处修改,而是直接重新定义了 text() 和 heading() 元素(来自 @metamask/snaps-sdk),使其内容先经过 escapeMarkdown() 函数处理,再进行渲染。这样,我们只需将所有包含同意消息的文件中的 import 表达式替换为我们的自定义元素,而无需逐一修复每处 text() 和 heading() 的调用,修复方案更加简洁。
有效的修复不仅能改善特定问题,还能预防性地防范未被发现的漏洞。
Diligence 点评 我们经常看到项目采取"打地鼠"式的安全策略,逐个修补出现的问题,而不考虑底层漏洞类别。通过解决问题的根本原因,而非局限于消除单一漏洞类别,MSQ 展示了他们将安全视为整体纪律,而非一系列补丁的理解。作为安全专家,我们只能建议客户不要急于修复,要花足够的时间将安全发现转化为对项目安全性产生积极影响的事件。
以上就是我们关于如何成功通过 MetaMask Snaps 安全审计的全部见解。遵循这些准则帮助 MSQ 团队获得了 Diligence 团队的正面评价报告。我们希望分享 MSQ 的经验能够帮助你做好安全审计准备,确保审计过程全面有效。
Consensys Diligence 已帮助区块链生态系统中的众多项目确保其协议做好上线准备,并为用户提供保护。我们很乐意针对你的 Snap 进行定制审计,检查上述及更多潜在漏洞。立即联系我们,获取报价!
AI 翻译。可能包含错误。请务必核实信息。