Как уже известно, легко добавить поддержку Serialization в выражение лямбда, когда целевой интерфейс еще не наследует Serializable
, как и (TargetInterface&Serializable)()->{/*code*/}
.
То, что я прошу, - это способ сделать обратное, явно удалить поддержку Serialization, когда целевой интерфейс наследует 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();
}
}
Но для выражения лямбда программист не имеет этого контроля над классом лямбда.
Зачем кому-то беспокоиться об удалении поддержки? Ну, помимо большого кода, созданного для поддержки поддержки 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
.
Решение о поддержке сериализации производится во время компиляции. Для этого требуется синтетический метод factory, добавленный в CreationSite
, а flag передан metafactory. Без флага генерируемая лямбда не поддерживает сериализацию, даже если интерфейс имеет наследование Serializable
. Класс лямбда даже будет иметь метод writeObject
, как в приведенном выше примере NotSupportingSerialization
. И без синтетического метода factory де-сериализация невозможна.
Это приводит к одному решению, я нашел. Вы можете создать копию интерфейса и изменить его, чтобы не наследовать Serializable
, а затем скомпилировать эту модифицированную версию. Поэтому, когда реальная версия во время выполнения наследует Serializable
, сериализация все равно будет отменена.
Ну, еще одно решение - никогда не использовать лямбда-выражения/ссылки на методы в соответствующем коде безопасности, по крайней мере, если целевой интерфейс наследует Serializable
, который всегда должен быть повторно проверен при компиляции с более новой версией интерфейса.
Но я думаю, что должны быть лучшие, предпочтительно, языковые решения.