作为 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");
}
}
这里,即使TargetInterface
是public
(接口(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/