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

2024-05-11

As 已经知道 https://stackoverflow.com/a/22808112/2711488很容易添加序列化当目标接口尚未继承时支持 lambda 表达式Serializable, 就像(TargetInterface&Serializable)()->{/*code*/}.

我要求的是一种相反的方法,即在目标接口时显式删除序列化支持does继承Serializable.

由于您无法从类型中删除接口,基于语言的解决方案可能看起来像(@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");
    }
}

这里,即使访问私有方法也不会暴露TargetInterface is public(接口方法总是public)只要程序员注意,不要传递实例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 and a flag http://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html#FLAG_SERIALIZABLE传递给元工厂 http://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html#altMetafactory-java.lang.invoke.MethodHandles.Lookup-java.lang.String-java.lang.invoke.MethodType-java.lang.Object...-方法。如果没有该标志,即使接口碰巧继承,生成的 lambda 将不支持序列化Serializable。 lambda 类甚至会有一个writeObject方法就像在NotSupportingSerialization上面的例子。如果没有合成工厂方法,反序列化是不可能的。

我发现这导致了一个解决方案。您可以创建该接口的副本并将其修改为不继承Serializable,然后针对该修改后的版本进行编译。所以当运行时的真实版本恰好继承时Serializable,连载仍会被撤销。

那么,另一个解决方案是永远不要在安全相关代码中使用 lambda 表达式/方法引用,至少在目标接口继承的情况下是如此Serializable在针对较新版本的接口进行编译时,必须始终重新检查。

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


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

您注意到:

那么,另一个解决方案是永远不要在安全相关代码中使用 lambda 表达式/方法引用,

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

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

public interface SomeLibType extends Runnable, Serializable { }

使用期望这种类型的方法:

public void gimmeLambda(SomeLibType r)

并且您想要将 lambda 传递给其中,但不让它们可序列化(并承担由此产生的后果。)因此,请自己编写这个辅助方法:

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

现在您可以调用库方法:

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

可以显式删除 lambda 的序列化支持 的相关文章

随机推荐