作为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");
}
}
这里,即使TargetInterface
为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
并将flag传递给metafactory方法。如果没有该标志,即使接口恰好继承Serializable
,生成的lambda也不支持序列化。 lambda类甚至可以使用上面writeObject
示例中的NotSupportingSerialization
方法。如果没有合成工厂方法,则不可能进行反序列化。
这导致了一个解决方案,我找到了。您可以创建接口的副本并将其修改为不继承Serializable
,然后针对该修改版本进行编译。因此,当运行时的实际版本继承Serializable
时,序列化仍将被撤销。
好吧,另一种解决方案是永远不要在安全相关代码中使用lambda表达式/方法引用,至少在目标接口继承Serializable
时必须始终重新检查,在针对较新版本的接口进行编译时
但我认为必须有更好的,最好是语言内的解决方案。
答案 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 $支持甚至不会存在。