cleanUrl: /posts/write-readObject-methods-defensively-effective-java-item-88
share: true

하나의 클래스를 직접 직렬화 하면서 무엇을 주의해야 하는지, Serializable 이 보안적인 측면에서 무엇을 주의해야 할지 다뤄본다. Serializable 을 지양하는 이유중에 하나가 보안인데 이 item 은 특히 이 부분을 대응하기 위해 어떻게 방어적인 패턴을 적용할지 살핀다.

88. Write readObject methods defensively

item 50 에서 Date 객체를 방어적으로 복사하느라 코드가 상당히 길어졌다. 이 클래스는 직렬화 해보자

public final class Period {
	private final Date start;
	private final Date end;

	/**
     * @param start 시작 시간
     * @param end 종료 시각; 시작 시간보다 뒤여야 한다.
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을때 발생한다
     * @throws NullPointerException start 나 end 가 null 이면 발생한다.
     */
	public Period(Date start, Date end) {
		if (start.compareTo(end) > 0)
			throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
		this.start = start;
		this.end = end;
	}

	public Date start() {
		return new Date(start.getTime());
	}

	public Date end() {
		return new Date(end.getTime());
	}
}

논리적 구현과 물리적 구현이 일치하여 기본 implements Serializable 만 붙여도 되겠다고 생각할 수 있지만 그렇지 않다.

이유는 이 클래스의 핵심적인 불변식을 보장하지 못하기 때문이다.

readObject 는 public API 이다.

public class BogusPeriod {

		// 진짜 Period 인스터느에서는 만들어질 수 없는 바이트 스트림
    private static final byte[] serializedForm = {
        (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);
    }

    // Returns the object with the specified serialized form
    static Object deserialize(byte[] sf) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

위 역직렬화가 내 컴퓨터에선 진행이 되지 않는데 아마도 package 이름이 일치하지 않아서 그런것 같다.

하지만 결과는 end < start 로 불변식이 깨지게 된다.

즉, 클래스가 지켜야하는 규칙을 무시하고 Period 가 생성된다.

이 문제를 고치기 위해 다음과 같은 코드가 추가 되어야 한다.

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    if (start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + "가 " + end + "보다 늦다.");
    }
}

악의적인 공격, 비잔틴 결함

start, end 는 final 변수이다. 하지만 이 참조에 접근하여 Date 인스턴스들을 수정할 수 있다.