티스토리 뷰

728x90
반응형

직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라


  • Serializable 인터페이스를 구현하는 순간 생성자와는 별개로 readObject 메서드로 인해 바이트 스트림을 매개변수로 받는 또 하나의 생성자가 생긴다고 하였습니다. 이러한 방법으로 인해 버그와 보안 문제가 발생할 수 있습니다. 하지만 직렬화 프록시 패턴을 사용하여 어느정도 위험을 해소할 수 있습니다.

💡 직렬화 프록시 패턴

  • 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static 으로 선언하는 중접 클래스를 만드는데 이 클래스가 바깥 클래스의 직렬화 프록시를 수행합니다.

특징

  • 중첩 클래스의 생성자는 단 하나여야 합니다.
  • 생성자는 바깥 클래스를 매개변수로 받아야 합니다.
  • 생성자의 역할은 단순히 인수로 넘어온 인스턴스의 데이터를 복사합니다.
  • 바깥 클래스, 직렬화 프록시 클래스 모두 Serializable 인터페이스를 구현해야 합니다.

💡 예제 코드

public final class Period implements Serializable {

    private static final long serialVersionUID = 1L;
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + "가" + end + "보다 늦습니다.");
        }
        this.start = start;
        this.end = end;
    }

    // 직렬화 프록시 패턴용 writeReplace 메서드
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // 직렬화 프록시 패턴용 readObject 메서드
    private void readObject(ObjectInputStream s) throws IOException {
        throw new InvalidObjectException("Proxy required !");
    }

    // 직렬화 프록시 클래스
    private static class SerializationProxy implements Serializable {

        private static final long serialVersionUID = 1L;
        private final Date start;
        private final Date end;

        public SerializationProxy(Period period) {
            this.start = period.start;
            this.end = period.end;
        }

        private Object readResolve() {
            return new Period(start, end);
        }
    }
}

 

💡 writeReplace 메서드

  • 해당 메서드의 역할은 자바 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하게 하는 역할을 수행합니다. 직렬화가 이루어지기 전에 바깥 클래스의 인스턴스를 직렬화 프록시 객체로 변경시켜 줍니다. 이 메서드로 인해 자바 직렬화 시스템은 바깥 클래스의 인스턴스를 생성할 수 없습니다. 하지만 공격자는 불변식을 훼손하고자 readObject 메서드를 사용하여 공격할 수 있습니다.
private Object writeReplace() {
    return new SerializationProxy(this);
}

 

💡 readObject 메서드

  • 공격자는 불변식을 훼손하고자 readObject 메서드로 공격을 가할 수 있지만 아래와 같이 작성하면 공격을 막을 수 있습니다.
  • readObject, writeObject 메서드가 있다면 기본적으로 Serialization 과정에서 ObjectInputStream, ObjetOutputStream이 호출하게 되는데 그 안에 커스텀 로직을 넣어 수행해도 됩니다.
private void readObject(ObjectInputStream s) throws IOException {
    // readObject는 역직렬화할 때(직렬화된 데이터를 다시 객체로 만들때) 
    // 아래처럼 하게되면 Period 객체로 역직렬화할 때 예외 발생시킴
    throw new InvalidObjectException("Proxy 필요!");
}

 

💡 readResolve 메서드

  • 바깥 클래스(Period)와 동일한 인스턴스를 반환하는 readResolve 메서드를 SerializationProxy 클래스에 추가하면 직렬화는 생성자를 이용하지 않고 인스턴스를 생성하는 기능을 제공하는데, 이 메서드를 사용하면 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩토리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하는 것입니다. 따라서 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 또 다른 수단을 강구하지 않아도 됩니다.
private Object readResolve() {
    return new Period(start, end);
}

 

💡 직렬화 프록시 패턴의 장점

  • 방어적 복사처럼 직렬화 프록시 패턴은 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단해줍니다.
  • 직렬화 프록시는 Period 필드를 final로 선언해도 무방하므로 Period 클래스는 불변식을 지킬 수 있습니다.
  • 어떤 필드가 기만적인 직렬화 공격의 목표가 될 지 고민하지 않아도 되며, 역직렬화할 때 유효성 검사를 수행하지 않아도 됩니다.

💡 직렬화 프록시 패턴의 한계

  • 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없습니다.
  • 객체 그래프에 순환이 있는 클래스에는 적용할 수 없습니다. 이런 객체의 메서드를 직렬화 프록시의 readResolve 안에서 호출하게 된다면 ClassCastException이 발생합니다. 그 이유눈 직렬화 프록시는 프록시일뿐 실제 객체는 아직 만들어지지 않았기 때문입니다.
  • 직렬화 프록시 패턴을 사용하게 되면 방어적 복사할때보다 성능이 느려집니다.

 

 

 

 

 

728x90
반응형