publicfinalclassPeriod { privatefinal Date start; privatefinal Date end; /** * @param start 시작 시각 * @param end 종료 시각; 시작 시각보다 뒤여야 한다. * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다. * @throws NullPointerException start나 end가 null이면 발행한다. */ publicPeriod(Date start, Date end) { this.start = newDate(start.getTime()); this.end = newDate(end.getTime()); if(this.start.compareTo(this.end) > 0) { thrownewIllegalArgumentException(start + "가 " + end + "보다 늦다."); } } public Date start() { returnnewDate(start.getTime()); } public Date end() { returnnewDate(end.getTime()); } public String toString() { return start + "-" + end; } }
이 클래스는 물리적 표현과 논리적 표현이 부합하므로 기본 직렬화를 사용해도 좋다. 하지만 이렇게 해서는 주요한 Date의 불변식을 보장하지 못한다.
필요하다면 매개변수를 방어적으로 복사하라
readObject 메서드는 실질적으로는 또 다른 public 생성자이기 때문에 생성자와 똑같은 수준으로 주의를 기울여야한다.
readObject 메서드에서 인수가 유효한지 검사해야하고 필요하다면 방어적으로 복사하라
readObject에서 이 작업을 제대로 하지 못하면 공격자는 쉽게 클래스의 불변식을 깨뜨릴 수 있다.
publicclassMutablePeriod { //Period 인스턴스 publicfinal Period period; //시작 시각 필드 - 외부에서 접근할 수 없어야 한다. publicfinal Date start; //종료 시각 필드 - 외부에서 접근할 수 없어야 한다. publicfinal Date end; publicMutablePeriod() { try { ByteArrayOutputStreambos=newByteArrayOutputStream(); ObjectArrayOutputStreamout=newObjectArrayOutputStream(bos); //유효한 Period 인스턴스를 직렬화한다. out.writeObject(newPeriod(newDate(), newDate())); /** * 악의적인 '이전 객체 참조', 즉 내부 Date 필드로의 참조를 추가한다. * 상세 내용은 자바 객체 직렬화 명세의 6.4절을 참고 */ byte[] ref = {0x71, 0, 0x7e, 0, 5}; // 참조 #5 bos.write(ref); // 시작 start 필드 참조 추가 ref[4] = 4; //참조 #4 bos.write(ref); // 종료(end) 필드 참조 추가 // Period 역직렬화 후 Date 참조를 훔친다. ObjectInputStreamin=newObjectInputStream(newByteArrayInputStream(bos.toByteArray())); period = (Period) in.readObject(); start = (Date) in.readObject(); end = (Date) in.readObject(); } catch (IOException | ClassNotFoundException e) { thrownewAssertionError(e); } } }
이 예시에서 Period 인스턴스는 불변식을 유지한 채 생성됐지만 의도적으로 내부의 값을 수정할 수 있었다. 이처럼 변경할 수 있는 Period 인스턴스를 획득한 공격자는 인스턴스가 불변이라고 가정하는 클래스에 넘겨 엄청난 보안 문제를 일으킬 수 있다.
이 문제의 근원은 Period의 readObject메서드가 방어적 복사를 충분히 하지 않은 데 있다. 객체를 직렬화할 때는 클라이언트가 소유해서는 안 되는 객체 참조를 갖는 필드를 모두 방어적으로 복사해야 한다. 따라서 readObject에서는 불변 클래스 안의 모든 private 가변 요소를 방어적으로 복사 해야한다.