自定义 ClassLoader 缺失 Sealed Package 校验导致包内类型注入
字数 2308
更新时间 2026-10-07 00:12:07

自定义 ClassLoader 缺失 Sealed Package 校验导致包内类型注入

当自定义 ClassLoader 自行读取 JAR 字节并直接调用 defineClass 时,JAR Manifest 中的 sealed package(封闭包)约束可能不会生效。封闭包一旦在运行时被定义成普通包,外部 JAR 中的同包名类就可能进入同一个运行时包,并访问包级私有成员。插件加载器、热更新加载器、脚本桥接加载器、内存字节码加载器都容易踩中这个坑。本文梳理其原理、成立前提与修复方式。

一、封闭包的边界假设

JAR Manifest 支持通过 Sealed: true 将包绑定到单一代码来源:包被封闭后,同一包名下的类只能继续从相同来源加载。这里的 sealed package 属于 JAR 与类加载阶段的约束,不是 Java 语言层面的 sealed class。

标准加载器(如 URLClassLoader)会维护这一语义:处理封闭包时先读取 Manifest,根据 Sealed 属性定义或校验包,最后才调用 defineClass。而一个只执行 defineClass 的自定义加载器,类定义动作本身不会自动读取 Manifest,也不会自动补充 sealed package 校验——于是 JAR 中声明的封闭包会在运行时退化为普通包。

二、底层原理:运行时包的双重身份

Java 的包级访问控制不只比较包名,还比较定义类加载器。两个类要达成“包级私有可互访问”,需要同时满足:包名相同 + 由同一个定义类加载器加载。sealed package 的约束正是依赖 Package 元数据中的“来源一致性”来生效。一旦加载器没有按 Manifest 把包记录为封闭,同包名但来自不同 JAR 的类就可能被归入同一运行时包,包级私有成员随之对“外人”开放。

三、包内类型注入的必要前提

这种“同包名类型注入”成立,需要同时满足几个条件:

  1. 受保护类与注入类使用相同包名;
  2. 注入类由同一个自定义 ClassLoader 定义;
  3. 该加载器未按 Manifest 调用 definePackage 记录封闭来源;
  4. 目标包中存在包级私有的类、方法或字段;
  5. 注入类存在可执行入口(插件入口、脚本桥接、服务加载或反射调用入口等)。

缺少“同一加载器”条件时,包名相同也不会形成同一运行时包;缺少可执行入口时,同包类型即使被定义也不会自然产生调用。需要强调:sealed package 的触发点在类加载阶段,而非源码编译阶段——注入类能通过编译(因为声明了同包名且 classpath 中存在目标类),不代表运行期一定能成功访问,真正决定行为的是 Package 的封闭状态与类来源 URL。

四、标准、失效与修复三种行为

  • 失效(缺陷加载器):某只读取字节、创建未封闭包再 defineClass 的加载器,会把声明为 Sealed: true 的包定义成 package sealed = false,随后将外部同包类定义进同一运行时包,使包级私有方法可被直接调用。注入调用并不使用反射、也不调用 setAccessible(true)、更不依赖命令行开放模块——它能成功,完全因为 JVM 在解析与校验访问时判定二者处于同一运行时包。
  • 标准(URLClassLoader)与修复(校验加载器):在不同 JAR 来源下都保持 package sealed = true,并拒绝来自另一 JAR 的同包类。
  • 同源合并 JAR 对照:把核心类与注入类放入同一个封闭 JAR 时仍可共存——说明 sealed package 限制的是来源一致性,而不是禁止同一来源内多个同包类共存。

五、修复:在 defineClass 前恢复封闭语义

修复位置必须在 defineClass 之前,核心策略:

  1. 读取 Manifest,按包定义或校验 Package;
  2. 对封闭包执行来源一致性检查:以与实际类来源一致的 sealBase(如 JAR 文件 URL)调用 isSealed(sealBase),若包已绑定到另一来源则拒绝定义,抛出 SecurityException。

若加载器不打算支持 sealed package,也不应静默忽略 Manifest——更安全的选择是:一旦发现 JAR Manifest 包含 Sealed: true 就拒绝加载,或禁止多个 JAR 定义同一包名。此外,若业务加载器允许外部字节码进入同一个定义加载器,还应将包名占用、JAR 来源与 Manifest 语义纳入加载前检查。

六、结语与防御认知

sealed package 不是加密机制,也不是权限沙箱;它只阻止不同代码来源继续向同一包追加类型。自定义加载器跳过 Manifest 处理后,JAR 打包方基于包边界建立的封装假设就会失效。由此可提炼一条通用认知:包级私有适合表达模块内部协作,不适合作为不可信代码的隔离边界——只要不可信字节码能由同一个加载器定义到同名包下,包级私有成员就可能被直接调用。需要真正隔离不可信代码时,应使用独立加载器、模块系统、SecurityManager/策略或进程级沙箱,而非依赖包可见性。


本文内容仅用于安全研究与类加载机制的工程加固之目的,所涉实验均为本地自有环境构造。请在授权范围内开展测试,遵守《中华人民共和国网络安全法》及相关法律法规。

相似文章
相似文章
小程序二维码
 全屏