Джош Блох
.pdfГлава 11 • Сериализация
экземпляре. Злоумышленник может |
прочесть экземпляр |
Peri |
od из ObjectlnputStream и получить |
«неконтролируемые |
ссылки |
на объекты», прилагаемые к этому потоку. Имея указанные ссылки, злоумышленник получает доступ к объектам, на которые есть ссылки в закрытых полях Date объекта Period. Меняя эти экземпляры Date, он может менять и сам экземпляр Period. Следующий класс демон стрирует атаку такого рода:
public class MutablePeriod {
//Экземпляр интервала времени public final Period period;
//Поле начала периода, к которому мы не должны иметь доступ public final Date start;
//Поле конца периода, к которому мы не должны иметь доступ public final Date end;
public MutablePeriod() { try {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out =
new ObjectOutputStream(bos);
//Сериализуем правильный экземпляр Period out.writeObject(new Period(new Date(), new Date()));
j*
*Добавляем в конец неконтролируемые «ссылки на предыдущие объекты»
* для внутренних |
полей Date в экземпляре Period. Подробнее см. |
* «Java Object |
Serialization Specification», раздел 6.4. |
*/
byte[] ref = 0x71, 0, 0x7e, 0, 5 ; // Ссылка № 5 bos.write(ref); // Поле start
ref[4] = 4; // Ссылка № 4 bos.write(ref); // Поле end
//Десериализация экземпляра Period и «украденных» ссылок
//на экземпляры Date
ObjectlnputStream in = new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray()));
420
С татья 76
period |
= (Period) in.readObject(); |
|
start |
= (Date) |
in.readObject(); |
end |
= (Date) |
in.readObject(); |
} catch (Exception e) {
throw new RuntimeException(e.toString());
}
}
}
Чтобы увидеть описанную атаку в действии, запустим следую щую программу:
public static void main(String[] args) { MutablePeriod mp = new MutablePeriod(); Period p = mp.period;
Date pEnd = mp.end;
//Переведем часы назад pEnd.setYear(78); System.out.println(p);
//Вернемся в шестидесятые! pEnd.setYear(69); System.out.println(p);
}
Запустив эту программу, имеем на выходе следующее:
Wed Apr 02 11:04:26 PDT 2008 - Sun Apr 02 11:04:26 PST 1978 Wed Apr 02 11:04:26 PDT 2008 - Wed Apr 02 11:04:26 PST 1969
Хотя экземпляр Period создается с неповрежденными инвари антами, при желании его внутренние компоненты можно поменять извне. Завладев изменяемым экземпляром класса Period, злоумыш ленник может причинить массу вреда, передав этот экземпляр клас су, чья безопасность зависит от неизменяемости класса Period. И это не такая уж надуманная тема. Существуют классы, чья безопасность зависит от неизменяемости класса String.
Причина этой проблемы кроется в том, что метод readObject клас са Period не выполняет необходимого резервного копирования. При
421
Глава 11 • Сериализация
десериализации объекта крайне важно создать резервные копии для всех полей, содержащих ссылки на те объекты, которые не должны попасть в распоряжение клиентов. Поэтому каждый сериализуемый неизменяемый класс, содержащий закрытые изменяемые компоненты, должен в своем методе readObject создавать резервные копии для этих компонентов. Следующий метод readObject достаточен для того, чтобы объект Period оставался неизменяемым и сохранялись его инва рианты:
// Метод readObject с резервным копированием и проверкой правильности private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException { s.defaultReadObject();
//Резервное копирование наших изменяемых компонент start = new Date(start.getTimeO);
end = new Date(end.getTimeO);
//Проверка ваших инвариантов
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +” after “+ end);
}
Заметим, что резервное копирование осуществляется перед про веркой корректности и что для резервного копирования не использу ется метод clone из класса Date. Указанные особенности реализации необходимы для защиты объекта Period (статья 39). Заметим также, что выполнить резервное копирование для полей final невозможно. Следовательно, для того чтобы можно было воспользоваться мето дом readObject, мы должны сделать поля start и end неокончатель ными. Это огорчает, но приходится выбирать из двух зол меньшее. Разместив в классе метод readObject и удалив модификатор final из полей start и end, мы обнаруживаем, что класс MutablePeriod по терял свою силу. Приведенная выше программа выводит теперь сле дующие строки:
Wed Apr 02 11:05:47 PDT 2008 - Thu Apr 02 11:05:47 PDT 2008 Wed Apr 02 11:05:47 PDT 2008 - Thu Apr 08 11:05:47 PDT 2008
422
С татья 76
Вверсии 1.4 методы writellnshared и readllnshared добавились
кObjectOutputStream с целью воспрепятствовать неконтролируемым
атакам ссылок на объекты за счет резервного копирования [Seri alization]. К сожалению, эти методы уязвимы к сложным атакам, похожим по своей природе на атаку ElvisStealer, описанную в ста тье 77. Не используйте методы writeUnshared и readllnshared. Они обычно быстрее, чем резервное копирование, но они не обеспечивают должных гарантий.
Есть простой безошибочный тест, показывающий, приемлем ли метод readObject, предлагаемый по умолчанию. Будете ли вы чув ствовать себя уютно, если добавите в класс открытый конструктор, который в качестве параметров принимает значения полей вашего объекта, записываемых в сериализованную форму, а затем заносит эти значения в соответствующие поля без какой-либо проверки? Если вы не можете ответить на этот вопрос утвердительно, вам нуж но явным образом реализовать метод readObject, который должен выполнять все необходимые проверки параметров и создавать все резервные копии, как это требуется от конструктора.
Между конструкторами и методами readObject существует еще одно сходство, касающееся расширяемых сериализуемых классов: метод readObject не должен вызывать переопределяемые методы ни прямо, ни косвенно (статья 17). Если это правило нарушено и вы зываемый метод переопределен, то он будет вызван прежде, чем бу дет десериализовано состояние соответствующего подкласса. Скорее всего, это приведет к сбою программы.
Подведем итоги. Всякий раз, когда вы пишете метод readObject, относитесь к нему как к открытому конструктору, который должен создавать правильный экземпляр независимо от того, какой поток байтов был ему передан. Не надо исходить из того, что полученный поток байтов действительно представляет сериализованный экзем пляр. Мы рассмотрели примеры классов, использующих сериализо ванную форму, предлагаемую по умолчанию. В той же степени все это относится к классам со специальными сериализованными фор
423
Глава 11 • Сериализация
мами. Приведем в кратком изложении рекомендации по написанию «пуленепробиваемого» метода readObject:
•Для классов, где есть поля, которые хранят ссылки на объект и при этом должны оставаться закрытыми, для каждого объ екта, который должен быть помещен в такое поле, необходи мо создавать резервную копию. В эту категорию попадают также изменяемые компоненты неизменяемых классов.
•Для классов, где есть инварианты, выполняйте проверку этих инвариантов и в случае ошибки инициируйте исключение 1п-
validObjectException. Проверка должна производиться по сле создания всех резервных копий.
•Если после десериализации необходимо проверить целый граф объектов, следует использовать интерфейс Objectln-
putValidation. Порядок применения этого интерфейса выхо дит за рамки данной книги. Пример можно найти в The Java Class Libraries, Second Edition, Volume /[Chan98, c. 1256].
•Ни прямо, ни косвенно не используйте в этом классе перео пределяемых методов.
Д л я к о н т р о л я над экземплярами предпочитайте использование перечислимых типов методу readResolve
В статье 3 описывается шаблон Singleton и приводится следую щий пример класса-синглтона. В этом классе есть ограничения на до ступ к конструктору с тем, чтобы гарантировать создание только од ного экземпляра:
public class Elvis {
public static final Elvis INSTANCE = new ElvisO; private ElvisO {
424
С татья 77
}
public void leaveTheBuilding { . }
Как отмечалось в статье 3, этот класс перестает быть синглтоном, если в его декларацию были добавлены слова «implements Serializ able». Не имеет значения, использует ли этот класс сериализованную форму, предлагаемую по умолчанию, или использует специальную форму (статья 73), предоставляется ли пользователю в этом классе явный метод readObject (статья 76). Любой метод readObject, яв ный или используемый по умолчанию, возвращает вновь созданный экземпляр, а не тот самый экземпляр, который был создан в момент инициализации класса.
Функция read Resolve позволяет нам заменить другой экземпляр на первый, созданный с помощью readObject [Serialization, 3.7]. Если класс десериализуемого объекта имеет метод readResolve с соответ ствующей декларацией, то по завершении десериализации этот метод будет вызван для вновь созданного объекта. Клиенту вместо ссылки на вновь созданный объект передается ссылка, возвращаемая этим методом. В большинстве случаев, когда используется этот механизм, ссылка на новый объект не сохраняется, а сам объект фактически яв ляется «мертворожденным» и немедленно становится объектом для сборки мусора.
Если класс Elvis создан так, чтобы реализовать интерфейс Seri alizable, то для того, чтобы обеспечить свойство синглтона, доста точно будет создать следующий метод readResolve:
//readResolve для контроля над экземплярами
private Object readResolve(){
//Возвращает только истинный экземпляр Elvis и дает возможность
//сборщику мусора позаботиться об Elvis--caM0 3 BaHu,e
return INSTANCE;
}
425
Глава 11 • Сериализация
Этот метод игнорирует десериализованный объект, просто воз вращая уникальный экземпляр Elvis, который был создан во время инициализации класса. По этой причине необходимо, чтобы в се риализованной форме экземпляра Elvis не содержалось никаких реальных данных, а все поля экземпляра были помечены как tran sient. На самом деле, если вы полагаетесь на метод readResolve для контроля, все поля со ссылками на объект должны быть объявлены как transient. В противном случае для определенного атакующего возможно обезопасить ссылку на десериализованный объект до того, как он запустит метод readResolve, используя прием, который ча стично похож на атаку MutablePeriod из статьи 76.
Атака немного сложнее, но основная идея проста. Если сингл тон содержит непереходное (nontransient) поле со ссылкой на объ ект, содержимое этого поля будет десериализовано до того, как бу дет запущен метод синглтона readResolve. Это позволяет осторожно созданному потоку «украсть» ссылку на исходный десериализуемый синглтон, в то время как содержимое поля ссылки на объект десери ализовано.
Вот как это работает. Сначала пишется класс-вор, у которого есть
иметод readResolve, и поле экземпляра, ссылающееся на сериализо ванный синглтон, который скрыт. В потоке сериализации происхо дит замена непереходного поля синглтона экземпляром класса-вора. Теперь у вас появилась цикличность: синглтон содержит класс-вор
икласс-вор ссылается на синглтон.
Поскольку синглтон содержит класс-вор, то метод этого клас са readResolve выполняется сначала тогда, когда синглтон десериа лизован. В результате, когда метод readResolve выполняется, поле его экземпляра все еще ссылается на частично десериализованный синглтон.
Метод класса-вора readResolve копирует ссылку из поля свое го экземпляра в статическое поле, чтобы она была доступна после выполнения метода readResolve. Затем метод возвращает значение корректного типа для поля, в котором он прячется. Если бы этого
426
С татья 77
не делалось, то виртуальная машина вывела бы ClassException при попытке системы сериализации сохранить украденную ссылку в это поле.
Для конкретности рассмотрим следующий прием взломанного синглтона:
//Взломанный синглтон - у него есть непереходное (nontransient)
//поле со ссылкой на объект!
public class Elvis implements Serializable { public static final Elvis INSTANCE = new ElvisO; private ElvisO { }
private String[] favoriteSongs =
{ "Hound Dog”, "Heartbreak Hotel" }; public void printFavorites() {
System.out.printIn(Arrays.toString(favoriteSongs));
}
private Object readResolve() { return INSTANCE;
}
}
А вот класс-вор, созданный как описано выше:
public class ElvisStealer implements Serializable { static Elvis impersonator;
private Elvis payload;
private Object readResolve() {
//Сохраняет ссылку в экземпляр Elvis impersonator = payload;
//Возвращает объект правильного типа для поля favoriteSongs return new String[] { "A Fool Such as I” };
}
private static final long serialVersionlllD = 0;
}
И наконец, ужасная программа, которая десериализует поток, сделанный руками для производства двух различных экземпляров
427
Глава 11 • Сериализация
испорченного синглтона. Метод десериализации опущен из програм мы, так как он идентичен такому же методу на странице 303:
public class Elvislmpersonator {
// Битовый поток не может прийти из реального экземпляра Elvis!
private |
static |
final |
byte[] serializedForm = new |
|
CDM |
i- |
|
|||||
|
|
|
|
|
|
|
|
|
Q_ |
i - |
|
|
|
|
|
|
|
|
|
|
|
>> |
1-1 |
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(byte)Oxac, |
(byte)Oxed, |
0x00, 0x05, 0x73, 0x72 , 0x00, 0x05, |
||||||||||
0x45, |
0x6c, |
0x76 |
, 0x69, |
0x73, |
(byte)0x84, |
(byte)0xe6, |
|
|||||
(byte)0x93, |
0x33 |
, (byte)0xc3, |
(byte)0xf4, |
(byte)0x8b, |
|
|||||||
0x32, |
0x02, |
0x00 |
, 0x01, |
0x4c, |
0x00, |
OxOd, 0x66 , 0x61, 0x76, |
||||||
0x6f, |
0x72, |
0x69 |
, 0x74, |
0x65, |
0x53, |
0x6f, 0x6e , 0x67, |
0x73, |
|||||
0x74, |
0x00, |
0x12 |
, 0x4c, |
0x6a, |
0x61, |
0x76, |
0x61 , 0x2f, 0x6c, |
|||||
0x61, |
0x6e, |
0x67 |
, 0x2f, |
0x4f, 0x62, |
0x6a, |
0x65,, 0x63, |
0x74, |
|||||
0x3b, |
0x78, |
0x70 |
, 0x73, |
0x72, |
0x00, |
0x0c, |
0x45,, 0x6c, |
0x76, |
||||
0x69, |
0x73, |
0x53 |
, 0x74, |
0x65, |
0x61, |
0x6c, |
0x65, |
|
0x72, |
0x00, |
||
0x00, |
0x00, |
0x00 |
, 0x00, |
0x00, |
0x00, |
0x00, |
0x02, |
|
0x00, |
0x01, |
||
0x4c, |
0x00, |
0x07 |
, 0x70, |
0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, |
||||||||
0x74, |
0x00, |
0x07 |
, |
0x4c, |
0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, |
|||||||
0x78, |
0x70, |
0x71 |
, 0x00, |
0x7e, |
0x00, |
0x02 |
|
|
|
|
|
|
}; |
|
|
main(String[] args) |
|
|
|
|
|
|
|||
public static void |
{ |
|
|
|
|
|
||||||
//Инициализирует ElvisStealer.impersonator и возвращает
//реальный Elvis (которым является Elvis.INSTANCE) Elvis elvis = (Elvis) deserialize(serializedForm);
Elvis impersonator = ElvisStealer.impersonator; elvis.printFavorites(); impersonator.printFavorites ();
}
}
Запуск этой программы приводит к следующему результату, до казывающему, что можно создать два различных экземпляра Elvis (с различными вкусами в музыке):
[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]
428
С татья 77
Вы можете решить проблему, объявив поле favo riteSong как tran sient но лучше проблему решить, сделав Elvis единственным элемен том перечислимого типа (статья 3). Исторически метод readResolve использовался для всех сериализуемых классов, контролируемых эк земплярами. Но с версии 1.5 это более не является лучшим способом поддерживать контроль экземпляров в сериализуемом классе. Как нам продемонстрировала атака ElvisStealer, данный прием хрупок и требует огромной осторожности.
Если вы напишете вместо этого сериализуемый класс, контроли руемый экземплярами, как перечислимый тип, у вас будет железная гарантия, что никаких экземпляров, кроме объявленных констант, не появится. Виртуальная машина Java даст вам такую гарантию, и вы можете на нее положиться. Но с вашей стороны потребуется особая забота. Вот как будет выглядеть пример с Elvis, если его сде лать как перечислимый тип:
// Синглтон как перечислимый тип - более предпочтительный подход public enum Elvis {
INSTANCE;
private String[] favoriteSongs =
{ “Hound Dog”, “Heartbreak Hotel” }; public void printFavorites() {
System.out.printIn(Arrays.toString(favoriteSongs));
}
}
Использование redResolve для контроля над экземплярами не устарело. Если вам нужно написать сериализуемый класс с управ ляемыми экземплярами, экземпляры которого не известны на момент компиляции, вы не сможете представить класс как перечислимый тип.
Доступ метода readResolve очень важен. Если вы поместите ме тод redResolve в завершенный класс, он должен быть закрытым. Если вы поместите метод readResolve в незавершенный класс, вам нужно внимательно отнестись к его доступу. Если он закрытый, то его дей ствие не будет распространяться на подклассы. Если он закрытый
429
