puppy-browser 安全深潜:悬垂指针、mXSS 与 Box::leak 内存泄漏背后的 3 个 Rust 安全教训

📅 2026/8/23 12:01:22
puppy-browser 安全深潜:悬垂指针、mXSS 与 Box::leak 内存泄漏背后的 3 个 Rust 安全教训
puppy-browser 安全深潜悬垂指针、mXSS 与 Box::leak 内存泄漏背后的 3 个 Rust 安全教训【免费下载链接】puppy-browserAn example implementation of a tiny Web browser for educational purposes.项目地址: https://gitcode.com/gh_mirrors/pu/puppy-browserpuppy-browser 是一个用于教学目的的迷你 Web 浏览器educational tiny Web browser用 Rust 实现能解析 HTML、运行 JavaScript 并在终端里渲染页面。别看它玩具属性强它的源码里却藏着 3 个真实的安全隐患V8 与 Rust 之间的悬垂指针dangling pointer、可通过属性注入触发的 mXSSmutation XSS变异 XSS以及Box::leak()带来的内存泄漏。这篇文章带你逐一定位这些隐患的代码位置并提炼出对 Rust 初学者最有用的安全教训。认识 puppy-browser为什么教学浏览器是最好的安全教材puppy-browser 的架构非常经典HTML 解析器位于 src/core/html.rs用combine组合子手写了一个非标准但可读的解析器把字节流变成 DOM 树DOM 模型src/core/dom/node.rs 中的Node/NodeType结构清晰、所有权关系一目了然JavaScript 引擎通过rusty_v8内嵌 V8 引擎src/javascript/binding/dom.rs 负责把 Rust 的 DOM 对象桥接成 JS 对象终端渲染src/tui/下的 TUI 模块负责把 DOM 画到屏幕上。想本地跑起来先读 docs/setup-dev.md构建较慢的话可以按 scripts/prepare-v8.sh 的说明缓存 V8 产物。正是这种Rust 堆内存 ↔ V8 堆内存的双向桥接让它成为讲解内存安全的绝佳样本。⬇️ 下面进入正题。漏洞一悬垂指针——V8 对象还指向已被替换的 Rust 节点这是最隐蔽的问题作者在源码里亲自在 src/javascript/binding/dom.rs 留了 TODOTODO (security): the setter might cause dangling pointer from v8 to rusts heap.它是怎么发生的JS 侧的每个 DOM 对象内部字段internal field里存了一个指向 Rust 堆的裸指针见 src/javascript/binding/dom.rs 中的set_node_internal_ref与to_linked_rust_node当页面脚本执行node.innerHTML ...时Rust 侧会整体替换子节点src/core/dom/node.rs 的set_inner_html旧的BoxNode被丢弃、内存回收但document.all等接口之前发给 JS 的对象其内部字段仍指向那块已被释放的 Rust 内存——于是悬垂指针诞生指针还在目标已亡。Rust 初学者的教训Rust 的所有权系统在纯 Rust 世界里帮你挡住了悬垂指针但一旦跨 FFI 边界把引用转成裸指针*mut Tunsafe安全网就撤掉了。谁持有指针、谁决定生命周期必须自己手动对齐。工程上的常见解法是给节点引入全局注册表 自增 ID让 V8 只持有票据而非裸指针。漏洞二mXSS 变异 XSS——一个没转义引号的属性序列化先看这段有味道的代码位于 src/core/dom/node.rs// TODO (security): do this securely! This might causes mXSS. format!({}\{}\, k, v)把 DOM 重新序列化成 HTML 字符串时属性值原样拼进双引号里没有对、等字符做任何转义。为什么这能构成 XSS攻击者先用安全的 DOM API 构造无害节点序列化时引号逃逸出属性边界注入出新的属性甚至标签。经典的 mXSS 就是这种先构造、再解析的变异手法。puppy-browser 的提交历史里还有一段真实攻防作者曾为 seccamp 安全练习粗糙地支持了quot;实体解码见 src/core/html.rs 中属性值解析处结果quot;被解析期还原成反而让上面的未转义序列化更容易被打穿——修实体这一刀意外给 mXSS 递了把刀。⚔️Rust 初学者的教训mXSS 的本质是数据在两次解析之间发生了语义漂移。序列化输出 HTML 必须走转义escape而且解析和序列化要使用同一套实体语义。安全漏洞常常不在你以为的地方而在两处各自看起来都无害的代码的接缝处。漏洞三Box::leak 内存泄漏——从不回收的节点引用再回到桥接代码。为了让 V8 的 internal field 能装下Rust 引用的地址set_node_internal_ref里做了一个经典也经典地危险的操作let boxed_ref Box::new(node_rust); let addr Box::leak(boxed_ref) as *mut NodeRefTarget as *mut c_void;Box::leak会把堆上的Box永久挂进全局堆指针从此无人可回收。作者也在 src/javascript/binding/dom.rs 明确标注TODO (enhancement): fix memory leak caused by Box::leak()也就是说JS 每拿到一个 DOM 对象Rust 堆就多一块永不释放的内存。页面脚本反复访问document.all内存只会涨、不会降——一个缓慢但确定的 OOM 隐患。Rust 初学者的教训Rust 没有 GC不等于Rust 没有泄漏。mem::forget、Box::leak、unsafe手动接管生命周期都是在向编译器借债必须配套写清偿还路径比如用Box::from_raw反向回收或改用ArcRefCell...这类带引用计数的共享所有权方案。凡是用 unsafe 打破的地方都要有对应的销毁协议。总结从 3 个 TODO 到 Rust 安全的通用清单puppy-browser 把教学浏览器写得很小却把浏览器引擎的安全难点浓缩得很全。给初学者的 3 条清单跨语言边界处所有权规则失效——FFI / unsafe / 裸指针处生命周期要由人保证别指望编译器序列化与解析必须语义一致——mXSS 往往诞生在两次解析的缝隙里输出 HTML 永远要转义unsafe 的债要登记在册——Box::leak、mem::forget都要有明确的回收方案代码里留 TODO 也要留偿还计划。这个仓库的价值恰恰在于它把大浏览器里最难的内存问题压缩进了几百行可读的 Rust 代码。如果你想深入某处实现不妨从 src/javascript/binding.rs 和 src/core/dom.rs 的模块入口读起配合cargo test亲手验证每个修复。【免费下载链接】puppy-browserAn example implementation of a tiny Web browser for educational purposes.项目地址: https://gitcode.com/gh_mirrors/pu/puppy-browser创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考