java - 可以显式删除对 lambda 的序列化支持

标签 java serialization lambda java-8

作为 already known当目标接口(interface)尚未继承 Serializable 时,很容易将 Serialization 支持添加到 lambda 表达式,就像 (TargetInterface&Serializable)()->{/*code */}.

我要求的是一种相反的方法,当目标接口(interface)确实继承Serializable时显式删除序列化支持。

由于您无法从类型中删除接口(interface),因此基于语言的解决方案可能看起来像 (@NotSerializable TargetInterface)()->{/* code */}。但据我所知,没有这样的解决方案。 (如果我错了,请纠正我,这将是一个完美的答案)

即使类实现了 Serializable 也拒绝序列化在过去是合法行为,并且在程序员控制的类中,模式如下所示:

public class NotSupportingSerialization extends SerializableBaseClass {
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
      throw new NotSerializableException();
    }
    private void readObject(java.io.ObjectInputStream in)
      throws IOException, ClassNotFoundException {
      throw new NotSerializableException();
    }
    private void readObjectNoData() throws ObjectStreamException {
      throw new NotSerializableException();
    }
}

但是对于 lambda 表达式,程序员无法控制 lambda 类。


为什么有人会为移除支持而烦恼?好吧,除了生成包含 Serialization 支持的更大代码之外,它还会产生安全风险。考虑以下代码:

public class CreationSite {
    public static void main(String... arg) {
        TargetInterface f=CreationSite::privateMethod;
    }
    private static void privateMethod() {
        System.out.println("should be private");
    }
}

这里,即使TargetInterfacepublic(接口(interface)方法总是public),也不会暴露对私有(private)方法的访问由于程序员要小心,不要将实例 f 传递给不受信任的代码。

但是,如果 TargetInterface 继承 Serializable,情况就会发生变化。然后,即使 CreationSite 从未分发实例,攻击者也可以通过反序列化手动构造的流来创建等效实例。如果上面例子的界面看起来像

public interface TargetInterface extends Runnable, Serializable {}

很简单:

SerializedLambda l=new SerializedLambda(CreationSite.class,
    TargetInterface.class.getName().replace('.', '/'), "run", "()V",
    MethodHandleInfo.REF_invokeStatic,
    CreationSite.class.getName().replace('.', '/'), "privateMethod",
    "()V", "()V", new Object[0]);
ByteArrayOutputStream os=new ByteArrayOutputStream();
try(ObjectOutputStream oos=new ObjectOutputStream(os)) { oos.writeObject(l);}
TargetInterface f;
try(ByteArrayInputStream is=new ByteArrayInputStream(os.toByteArray());
    ObjectInputStream ois=new ObjectInputStream(is)) {
    f=(TargetInterface) ois.readObject();
}
f.run();// invokes privateMethod

请注意,攻击代码不包含任何 SecurityManager 会撤销的操作。


支持序列化的决定是在编译时做出的。它需要一个合成工厂方法添加到 CreationSite 和一个 flag传递给 metafactory方法。如果没有该标志,即使接口(interface)恰好继承了 Serializable,生成的 lambda 也将不支持序列化。 lambda 类甚至会有一个 writeObject 方法,就像上面的 NotSupportingSerialization 示例一样。而没有合成工厂方法,反序列化是不可能的。

我找到了一个解决方案。您可以创建接口(interface)的副本并将其修改为不继承 Serializable,然后针对修改后的版本进行编译。所以当运行时的真实版本恰好继承了Serializable,Serialization依然会被撤销。

好吧,另一个解决方案是永远不要在安全相关代码中使用 lambda 表达式/方法引用,至少如果目标接口(interface)继承 Serializable 必须始终重新检查,当针对较新版本进行编译时界面。

但我认为必须有更好的,最好是语言解决方案。

最佳答案

如何处理可串行化是 EG 面临的最大挑战之一;可以说没有很好的解决方案,只有在各种缺点之间进行权衡。有些人坚持所有的 lambdas 都可以自动序列化(!);其他人坚持认为 lambda 永远不能序列化(这有时似乎是一个很有吸引力的想法,但遗憾的是会严重违反用户的期望。)

请注意:

Well, another solution is to never use lambda expressions/method references in security relevant code,

事实上,序列化规范现在就是这么说的。

但是,这里有一个相当简单的技巧来做你想做的事。假设您有一些需要可序列化实例的库:

public interface SomeLibType extends Runnable, Serializable { }

使用需要这种类型的方法:

public void gimmeLambda(SomeLibType r)

并且您想将 lambdas 传递给它,但不让它们可序列化(并承担后果。)所以,为自己编写这个辅助方法:

public static SomeLibType launder(Runnable r) {
    return new SomeLibType() {
        public void run() { r.run(); }
    }
}

现在你可以调用库方法了:

gimmeLambda(launder(() -> myPrivateMethod()));

编译器会将您的 lambda 转换为不可序列化的 Runnable,并且洗钱包装器将使用满足类型系统的实例包装它。当您尝试对其进行序列化时,这将失败,因为 r 不可序列化。更重要的是,您不能伪造对私有(private)方法的访问权限,因为捕获类所需的 $deserializeLambda$ 支持甚至都不存在。

关于java - 可以显式删除对 lambda 的序列化支持,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25443655/

相关文章:

c++ - 传递 boost discrete_interval 作为此参数会丢弃限定符 [-fpermissive]

java - 查询参数名称可以是可变的吗?像: @QueryParam ("//anything")

java - Jackson JSON objectmapper 反序列化为 LinkedHashMap 而不是 HashMap

apache-spark - 将数据从 Spark-Streaming 存储到 Cassandra 时出现问题

c# - 如何将异常序列化为 Json

node.js - AWS Lambda 中是否有类似 Request Scoped with Node 的东西?

java - 执行流操作 java 8 时正在更新集合

java - 如何在 hbm.xml 中的 <key-many-to-one > 映射中设置 fetch ="Join"和 lazy ="False"

java - HashMap 获取输入的排序值 + 将最高值打印为字符串和整数

c# - 为什么 Lambda 的 MethodBody 在调用后会发生变化?