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

时间:2014-08-22 09:22:58

标签: java serialization lambda java-8

作为already known,当目标接口尚未继承Serializable时,很容易将序列化支持添加到lambda表达式,就像(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");
    }
}

这里,即使TargetInterfacepublic(接口方法总是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并将flag传递给metafactory方法。如果没有该标志,即使接口恰好继承Serializable,生成的lambda也不支持序列化。 lambda类甚至可以使用上面writeObject示例中的NotSupportingSerialization方法。如果没有合成工厂方法,则不可能进行反序列化。

这导致了一个解决方案,我找到了。您可以创建接口的副本并将其修改为不继承Serializable,然后针对该修改版本进行编译。因此,当运行时的实际版本继承Serializable时,序列化仍将被撤销。

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

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

1 个答案:

答案 0 :(得分:26)

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

你注意到:

  

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

事实上,序列化规范现在正是如此。

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

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不可序列化,因此会失败。更重要的是,您无法伪造对私有方法的访问权限,因为捕获类中所需的$ deserializeLambda $支持甚至不会存在。

相关问题