显式删除对 lambda 的序列化支持的可能性

2022-08-31 23:43:23

众所周知,当目标接口尚未继承时,很容易将序列化支持添加到 lambda 表达式中,就像 .Serializable(TargetInterface&Serializable)()->{/*code*/}

我要求的是一种相反的方法,当目标接口确实继承时,显式删除序列化支持。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");
    }
}

在这里,只要程序员小心,对私有方法的访问也不会公开,即使是(接口方法总是),不要将实例传递给不受信任的代码。TargetInterfacepublicpublicf

但是,如果继承 ,则情况会发生变化。然后,即使从不分发实例,攻击者也可以通过反序列化手动构造的流来创建等效实例。如果上述示例的接口如下所示TargetInterfaceSerializableCreationSite

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


支持序列化的决定是在编译时做出的。它需要将合成工厂方法添加到元工厂方法,并将标志传递给元工厂方法。如果没有该标志,生成的 lambda 将不支持序列化,即使接口碰巧继承 。lambda 类甚至会有一个方法,如上面的示例所示。如果没有合成工厂方法,反序列化是不可能的。CreationSiteSerializablewriteObjectNotSupportingSerialization

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

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

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


答案 1

如何处理可序列化是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,而清洗包装器会用一个满足类型系统的实例来包装它。当您尝试序列化它时,这将失败,因为 不可序列化。更重要的是,您无法伪造对私有方法的访问权限,因为捕获类中所需的$deserializeLambda$支持甚至不存在。r


答案 2