防范未授权反序列化远程代码执行:从对象白名单校验到 CI 门禁落地

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
341
文章
0
粉丝
信息安全1 49字数 1211阅读4分2秒阅读模式
AI智能摘要
后端系统常把内部数据流默认为可信,却不知未授权反序列化接口可能成为远程代码执行的高危通道。攻击者只需构造含恶意调用链的载荷,便能在对象恢复的瞬间完成代码执行。文章从 Java 原生序列化到 Jackson 多态反序列化,剖析了风险成因,并给出基于对象白名单的前置类型校验方案,以及将 Semgrep 静态检测沉淀为 CI 门禁的落地路径。你的反序列化防线,真的经得起一次恶意载荷的考验吗?
— AI 生成的文章内容摘要

许多后端系统在设计跨进程通信、本地缓存或状态持久化时,常假定传入的数据流是可信且受控的。正是这种“内部环境安全”的隐式假设,让未授权反序列化接口演变为高危的远程代码执行(RCE)通道。当反序列化器在缺乏类型范围约束的情况下重建对象时,攻击者可以通过构造包含恶意调用链(Gadget Chain)的载荷,在对象恢复属性或触发特定生命周期方法(如 readObject)的瞬间完成代码执行。

未授权反序列化攻击拦截与对象白名单过滤概念图

反序列化接口的代码风险成因

在 Java 原生序列化或各类第三方库(如 Jackson 多态反序列化、SnakeYAML、XStream)中,动态确定目标类型的机制为利用链构造提供了土壤。以 Java 原生接口为例,当代码直接暴露 ObjectInputStream.readObject() 时,类型转换发生在对象完全实例化之后:

// 危险示例:未做任何类型限制直接读取不可信输入
public Object processPayload(byte[] rawData) throws Exception {
    try (ByteArrayInputStream bais = new ByteArrayInputStream(rawData);
         ObjectInputStream ois = new ObjectInputStream(bais)) {
        // 在完成显式强转前,目标类的构造逻辑与 readObject 已经执行完毕
        return ois.readObject();
    }
}

在上述逻辑中,哪怕代码后续仅将返回值强转为业务所需的 UserSession,恶意载荷中的 Gadget 也会在 ois.readObject() 执行期间触发。只要运行环境类路径(Classpath)中存在可利用的类库,攻击者就能借助链式反射调用甚至 JNDI 引用触发任意代码执行。

基于严格对象白名单的安全重构

根治此类问题的核心是前置类型决议,即在真正实例化对象之前对二进制流中的类标识符做强校验。

对于 Java 原生序列化,从 Java 9 起引入了标准的 ObjectInputFilter(JEP 290 机制,亦向后移植到了 Java 8 的高版本中)。重构时应直接绑定白名单过滤器,仅允许业务必需的基础类型与特定实体反序列化,并严格限制对象图的深度和数组大小。

import java.io.*;

public class SecureDeserializer {
    // 仅允许基础类与指定的业务传输对象
    private static final ObjectInputFilter STRICT_FILTER = 
        ObjectInputFilter.Config.createFilter(
            "com.example.dto.UserSession;java.lang.String;java.lang.Number;!*"
        );

    public static Object safeRead(byte[] data) throws IOException, ClassNotFoundException {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bais)) {
            // 将过滤器挂载到当前流
            ois.setObjectInputFilter(STRICT_FILTER);
            return ois.readObject();
        }
    }
}

针对使用 Jackson 等 JSON 序列化库的场景,应避免全局开启 enableDefaultTyping(),改用显式注解(如 @JsonTypeInfo 配合 @JsonSubTypes)定义受信任的子类列表。

重构边界条件与副作用

  1. 类型演进影响:当实体类新增依赖其它非基础数据结构时,必须同步更新白名单模式串,否则会导致合法请求抛出 InvalidClassException。
  2. 回退与兼容:对于跨版本升级系统的临时过渡,应建立可配置的过滤器模式,但严禁将默认策略退化为通配允许。

CI/CD 静态规则检测与门禁落地

防御方案不仅依赖开发自觉,更需要将检测手段沉淀为流水线自动化门禁。使用基于抽象语法树(AST)的静态分析工具(如 Semgrep),能够在代码提交和 PR 审查阶段捕获危险的无保护反序列化调用。

CI/CD 流水线中针对不安全调用的静态规则拦截展示

Semgrep 静态审计规则配置

创建 .semgrep/rules/untrusted-deserialization.yml 规则文件,用于匹配缺乏 setObjectInputFilter 保护的 ObjectInputStream 实例:

rules:
  - id: java-untrusted-deserialization-no-filter
    languages: [java]
    message: "检测到可能存在未受保护的原生反序列化调用。请确保配置 ObjectInputFilter 白名单。"
    severity: ERROR
    patterns:
      - pattern: (ObjectInputStream $OIS).readObject(...)
      - pattern-not-inside: |
          $OIS.setObjectInputFilter(...);
          ...
          $OIS.readObject(...);

流水线配置与阻断策略

在 CI 流水线(以 GitHub Actions 为例)中执行静态检测,并设置阻断级别:

name: Security SAST Gate

on:
  pull_request:
    branches: [main, release/*]

jobs:
  sast-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Run Semgrep SAST
        uses: returntocorp/semgrep-action@v1
        with:
          config: .semgrep/rules/
          # 发现 ERROR 级别规则立即阻断流水线构建
          auditOn: "pull_request"
        continue-on-error: false

误报处理与回滚策略

  • 误报处置:如果部分单元测试需要模拟畸形流或已在严格隔离的离线环境运行,必须采用精确的代码行级忽略(如在代码中添加 // nosemgrep: java-untrusted-deserialization-no-filter),并强制要求提交对应的安全审批凭证,杜绝全局禁用规则。
  • 门禁回滚:当新引入的白名单规则阻断了关键产线迭代且确认属于规则误伤时,应通过 CI 环境变量降级该规则为 WARNING 级别,恢复分支合入,同时建立工单在限定周期内修正规则匹配模式,避免安全基线失守。

 
枫少@KillBoy
    • 青冥公子
      青冥公子 1

      白名单过滤这个思路确实比事后排查靠谱多了

    匿名

    发表评论

    匿名网友

    拖动滑块以完成验证