Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Метод readObject должен создаваться с защитой



 

В статье 24 представлен неизменяемый класс для интервалов времени, который содержит изменяемые закрытые поля даты. Чтобы сохранить свои инварианты и не­изменяемость, этот класс создает резервную копию объектов Date в конструкторе и методах доступа. Приведем этот класс:

 

 

210

 

import java.util.*;

import java.io.*;

 

public final class Period implements Serializable {

private final Date start;

private final Date end;

 

/**

* @ param start - начало периода

* @ param end - конец периода, не должен предшествовать началу периода

* @ throws IllegalArgument – если начало периода указано после конца

* @ throws NullPointerException – если начало или конец периода нулевые

*/

public Period(Date start, Date end) {

   this.start = new Date(start.getTime());

   this.end = new Date(end.getTime());

 

   if (this.start.compareTo(this.end) > 0)

     throw new IllegalArgumentException(start +" > "+ end);

}

 

public Date start () { return (Date) start.clone(); }

 

public Date end () { return (Date) end.clone(); }

 

public String toString() { return start + " - " + end; }

 

// ... // Остальное опущено

Предположим, что вам необходимо сделать этот класс сериализуемым. Посколь­ку физическое представление объекта Period в точности отражает его логическое содержание, вполне можно воспользоваться сериализованной формой, предлагаемой по умолчанию (статья 55). Может показаться, что все, что вам нужно для того, чтобы класс был сериализуемым,- это добавить в его декларацию слова "implements Serializable". Если вы поступите таким образом, то гарантировать классу сохранение его критически важных инвариантов будет невозможно.

Проблема заключается в том, что метод readObject фактически является еще одним открытым конструктором и потому требует такого же внимания, как и любой другой конструктор. Точно так же, как конструктор, метод readObject должен проверять правильность своих аргументов (статья 23) и при необходимости создавать для параметров резервные копии. Если метод readObject не выполнит хотя бы одно из этих условий, злоумышленник сможет относительно легко нарушить инварианты этого класса.

 

211

 

 

Метод readObject - это конструктор, который в качестве единственного вход­ного параметра принимает поток байтов. В нормальных условиях этот поток байтов создается в результате сериализации нормально построенного экземпляра. Проблема возникает, когда метод readObject сталкивается с потоком байтов, полученным искус­ственно с целью генерации объекта, который нарушает инварианты этого класса. Допустим, что мы лишь добавили "implements Serializable" в декларацию класса Period. Следующая уродливая программа генерирует такой экземпляр класса Period, в котором конец периода предшествует началу:

import java.io.*;

public class BogusPeriod {

 // Этот поток байтов не мог быть получен из реального экземпляра Period

private static final byte[] serializedForm = new byte[] {

(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,

0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,

0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,

0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,

0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,

0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,

0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,

0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,

0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,

(byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,

0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,

0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,

0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,

0x00, 0x78 };

public static void main(String[] args) {

   Period p = (Period) deserialize(serializedForm);

  System.out.println(p); }   // Возвращает объект с указанной сериализованной формой

public static Object deserialize(byte[] sf) {

   try {

       InputStream is = new ByteArrayInputStream(sf);

       ObjectInputStream ois = new ObjectInputStream(is);

       return ois.readObject();

   } catch (Exception e) {

       throw new IllegalArgumentException(e.toString());

   } } }

 

212

 

 

Фиксированный массив байтов, используемый для инициализации массива SеrializedForm, был получен путем сериализации обычного экземпляра Реriod и последующего редактирования потока байтов вручную. Детали построения потока для данного примера значения не имеют, однако если вам это любопытно, формат потока байтов описан в "Java тм Object Serializatioп Spec if icatioп" [Serialization, 6]. Если вы запустите эту программу, она напечатает: "Fri Jan 01 12: 00: 00 PST 1999 ­Suп Jan 01 12:00:00 PST 1984". Таким образом, то, что класс Period стал сериали­зуемым, позволило создать объект, который нарушает инварианты этого класса. Для решения этой проблемы создадим в классе Period метод readObject, который будет вызывать defaultReadObject и проверять правильность десериализованного объекта. Если проверка покажет ошибку, метод readObject инициирует исключение !nvalidObjectException, что не позволит закончить десериализацию:

private void readObject(ObjectlnputStream s)

throws IOException, ClassNotFoundException {

s.defaultReadObject();

// Проверим правильность инвариантов

 if (start.compareTo(end) > 0)

throw new InvalidObjectException(start +" after "+ end); }

Это решение не позволит злоумышленнику создать неправильный экземпляр класса Period. Однако здесь притаилась еще одна, более тонкая проблема. Можно создать изменяемый экземпляр Ре riod, Сфабриковав поток байтов, который начинается по­током байтов, представляющим прав ильный экземпляр Period, а затем формирует дополнительные ссылки на закрытые поля Date в этом экземпляре. Злоумышленник может прочесть экземпляр Period из ObjectlnputStream и получить "неконтроли­руемые ссылки на объекты", прилагаемые к этому потоку. Имея указанные ссылки, злоумышленник получает доступ к объектам, на которые есть ссылки в закрытых полях Date объекта Period. Меняя эти экземпляры Date, он может менять и сам экземпляр Period. Следующий класс демонстрирует атаку такого рода:

 

import java.util.*;

import java.io.*;

 

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);

 

213

 

// Сериализуем правильный экземпляр Period

       out.writeObject(new Period(new Date(), new Date()));

 

/*

* Добавляем в конец не контролируемые "ссылки

* на предыдущие объекты " для внутренних полей Date

 * в экземпляре Period . Подробнее см. "Java Object * Serialization Specification", раздел 6.4.

*/

byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5

       bos.write(ref); // The start field

       ref[4] = 4; // Ref # 4

       bos.write(ref); // The end field

 

       // Десериализация экземпляра Period и "украденных" ссылок

          // на экземпляры Date

       ObjectInputStream in = new ObjectInputStream(

       new ByteArrayInputStream(bos.toByteArray()));

       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;

 

   // Let's turn back the clock

   pEnd.setYear(78);

   System.out.println(p);

 

   // Bring back the 60's!

   pEnd.setYear(69);

   System.out.println(p);

}

}

 

 

214

 

 

Запустив эту программу, на выходе получим следующее:

Wed Маг 07 23:30:01 PST 2001 - Tue Маг 07 23:30:01 PST 1978

Wed Маг 07 23:30:01 PST 2001 - Fri Маг 07 23:30:01 PST 1969

Хотя экземпляр Реriod создается с неповрежденными инвариантами, при желании его внутренние компоненты можно поменять извне. Завладев изменяемым экземпляром класса Period, злоумышленник может причинить массу вреда, передав этот экземпляр классу, чья безопасность зависит от неизменяемости класса Period. И это не такая уж надуманная тема. Существуют классы, чья безопасность зависит от неизменяемости класса String.

Причина этой проблемы кроется в том, что метод readObject класса Реriod не выполняет необходимого резервного копирования. При десериализации объекта крайне важно создать резервные копии для всех полей, содержащих ссылки на те объекты, которые не должны попасть в распоряжение клиентов. Поэтому каждый сериализуемый неизменяемый класс, содержащий закрытые изменяемые компоненты, должен в своем методе readObject создавать резервные копии для этих компонентов. Следующий метод readObject достаточен для того, чтобы объект Реriod оставался неизменяемым и сохранялись его инварианты:

 

private void readObfect(ObjectInputStream s)

throws IOException, ClassNotFoundException {

s.defaultReadObject();

// Резервное копирование изменяемых компонентов

start = new Date(start.getTime());

            end = new Date(end.getTime());

// Проверка инвариантов

if (start.compareTo(end) > 0)

throw new InvalidObjectException(start +" after "+ end);

}

Заметим, что резервное копирование осуществляется перед проверкой коррект­ности и что для резервного копирования не используется метод clone из класса Date. Указанные особенности реализации необходимы для защиты объекта Period (статья 24). Заметим также, что выполнить резервное копирование для полей final невозможно. Следовательно, для того чтобы можно было воспользоваться методом readObject, мы должны сделать поля start и end неокончательными. Это огорчает, но приходится выбирать из двух зол меньшее. Разместив в классе метод readObject и удалив модификатор final из полей start и end, мы обнаруживаем, что класс MutablePeriod потерял свою силу. Приведенная выше программа выводит теперь следующие строки:

 

Thu Маг 08 00:03:45 PST 2001 - Thu Маг 08 00:03:45 PST 2001

Thu Маг 08 00:03:45 PST 2001 - Thu Маг 08 00:03:45 PST 2001

 

 

215

 

Есть простой безошибочный тест, показывающий, приемлем ли метод readObject, предлагаемый по умолчанию. Будете ли вы чувствовать себя уютно, если добавите в класс открытый конструктор, который в качестве параметров принимает значения полей вашего объекта, записываемых в сериализованную форму, а затем заносит эти значения в соответствующие поля без какой-либо проверки? Если вы не можете ответить на этот вопрос утвердительно, вам нужно явным образом реализовать метод readObject, который должен выполнять все необходимые проверки параметров и создавать все резервные копии, как это требуется от конструктора.

Между конструкторами и методами readObject существует еще одно сходство, касающееся расширяемых сериализуемых классов: метод readObject не должен вызы­вать переопределяемые методы ни прямо, ни косвенно (статья 15). Если это правило нарушено и вызываемый метод переопределен, то он будет вызван прежде, чем будет десериализовано состояние соответствующего подкласса. Скорее всего, это приведет к сбою программы.

Подведем итоги. Всякий раз, когда вы пишите метод readObject, относитесь к нему как к открытому конструктору, который должен создавать правильный эк­земпляр независимо от того, какой поток байтов был ему передан. Не надо исходить из того, что полученный поток байтов действительно представляет сериализованный экземпляр. Мы рассмотрели примеры классов, использующих сериализованную форму, предлагаемую по умолчанию. В той же степени все это относится к классам со специальными сериализованными формами. Приведем в кратком изложении реко­мендации по написанию "пуленепробиваемого" метода readObject:

· для классов, где есть поля, которые хранят ссылки на объект

и при этом должны оставаться закрытыми, для каждого объекта, который должен быть помещен в такое поле, необходимо создавать резервную копию. В эту категорию попадают также изменяемые компоненты неизменяемых классов.

 

· Для классов, где есть инварианты, выполняйте проверку этих инвариантов и в случае ошибки инициируйте исключение InvalidObjectException. Проверка должна производиться после создания всех резервных копий.

· Если после десериализации необходимо проверить целый гр~ф объектов, следует использовать интерфейс ObjectlnputValidation.

Порядок применения этого интерфейса выходит за рамки данной книги. Пример можно найти в The Java Class Libraries. Seco п d Editio п . Voluтe 1 (Chan98, стр. 1256].

 

· Ни прямо, ни косвенно не используйте в этом классе переопределяемых методов.

 

Как альтернативу защищенному методу readObject можно использовать метод readResolve. Об этом говорится в статье 57.

 

216

 

 


Поделиться:



Последнее изменение этой страницы: 2019-04-11; Просмотров: 188; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.05 с.)
Главная | Случайная страница | Обратная связь