[Effective Java] 아이템 88. readObject 메서드는 방어적으로 작성하라

2023. 1. 3. 23:36· BE/Java
목차
  1. 아이템 88. readObject 메서드는 방어적으로 작성하라
  2. readObject 메서드 Tip
  3. 정리
반응형

EFFECTIVE JAVA(이펙티브 자바)

 

 

이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.


아이템 88. readObject 메서드는 방어적으로 작성하라

ex 1) 방어적 복사를 사용하는 불변 클래스 예시

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) {
				// 가변인 Date 클래스의 위험을 막기 위해 방어적 복사 진행
        this.start = new Date(start.getTime()); 
        this.end = new Date(end.getTime());

        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
    }

    public Date start() { return new Date(start.getTime()); }
    public Date end() { return new Date(end.getTime()); }
    public String toString() { return start + " - " + end; }
    
    // ... 나머지 코드는 생략
}
  • 불변식을 지키고 불변을 유지하기 위해 생성자와 접근자에서 Date 객체를 방어적으로 복사하였다.
  • 이 클래스를 직렬화하기로 결정했다고 가정해보면, Period 객체의 물리적 표현이 논리적 표현과 부합하므로 기본 직렬화 형태를 사용해도 나쁘지 않다.
  • 이 클래스 선언에 implements Serializable 만 추가하게 되면 불변식을 보장할 수 없다.
    • readObject 메서드가 실질적으로 또 다른 public 생성자이기 때문이다.
    • 보통의 생성자처럼 readObject 메서드에서도 인수가 유효한지 검사해야 하고 필요하다면 매개변수를 방어적으로 복사해야 한다.
    • readObject가 이 작업을 제대로 수행하지 못하면 공격자는 손쉽게 해당 클래스의 불변식을 깨뜨릴 수 있다.
  • 즉, readObject는 매개변수로 바이트 스트림을 받는 생성자라 할 수 있다.
    • 보통의 경우 바이트 스트림은 정상적으로 생성된 인스턴스를 직렬화해 만들어지지만, 불변식을 깨뜨릴 의도로 임의로 생성한 바이트 스트림을 건네면 문제가 발생한다.
    • 정상적인 생성자로 만들 수 없는 객체를 생성할 수 있기 때문이다.

 

 

ex 2) 유효성 검사를 수행하는 readObject 메서드 (보완 필요)

private void readObject(ObjectInputStream s)
        throws IOException, ClassNotFoundException {
    // 불변식을 만족하는지 검사한다.
    if (start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}
  • 위의 유효성 검사로 허용되지 않는 Period 인스턴스를 생성하는 일은 막을 수 있지만, 문제가 하나 더 있다.
  • 정상 Period 인스턴스에서 시작된 바이트 스트림 끝에 private Date 필드로의 참조를 추가하면 가변 Period 인스턴스를 만들어 낼 수 있다.
  • 공격자는 ObjectInputStream에서 Period 인스턴스를 읽은 후 스트림 끝에 추가된 ‘악의적인 객체 참조’를 읽어 Period 객체의 내부 정보를 얻을 수 있다.
  • 이제 이 참조로 얻은 Date 인스턴스들을 수정할 수 있으니, Period 인스턴스는 불변이 아니게 되는 것이다.

 

 

ex 3) 가변 공격의 예시

public class MutablePeriod {
    // Period 인스턴스
    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()));

            /*
             * 악의적인 '이전 객체 참조', 즉 내부 Date 필드로의 참조를 추가한다.
             */
            byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // 참조 #5
            bos.write(ref); // 시작(start) 필드
            ref[4] = 4; // 참조 # 4
            bos.write(ref); // 종료(end) 필드

            // Period 역직렬화 후 Date 참조를 '훔친다.'
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new AssertionError(e);
        }
    }

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

        // 60년대로 회귀!
        pEnd.setYear(69);
        System.out.println(p);
    }
}
  • 이 예에서 Period 인스턴스는 불변식을 유지한 채 생성됐지만, 의도적으로 내부의 값을 수정할 수 있다.
  • 이처럼 변경할 수 있는 Period 인스턴스를 획득한 공격자는 이 인스턴스가 불변이라고 가정하는 클래스에 넘겨 보안 문제를 발생시킬 수 있다.
  • 실제로도 보안 문제를 String이 불변이라는 사실에 기댄 클래스들이 존재한다.
  • 객체를 역직렬화할 때는 클라이언트가 소유해서는 안 되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다.
  • 따라서, readObject에서 불변 클래스 안의 모든 private 가변 요소를 방어적으로 복사해야 한다.

 

 

 

ex 4) 방어적 복사와 유효성 검사를 수행하는 readObject 메서드 예시

private void readObject(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 + " 가 " + end + " 보다 늦다.");
    }
}
  • 방어적 복사를 유효성 검사보다 앞서 수행하며, Date의 clone 메서드는 사용하지 않았다.
  • 두 조치 모두 Period를 공격으로부터 보호하는 데 필요하다.
  • 또한, final 필드는 방어적 복사가 불가능하니 주의하자.
    • 위의 이유로 이 readObject 메서드를 사용하려면 start와 end 필드에서 final 한정자를 제거해야 한다.
    • 아쉽지만 앞서 살펴본 공격 위험에 노출되는것 보다 낫다.

 

 

 

readObject 메서드 Tip

  • 기본 readObject 메서드를 사용해도 좋을 지 판단하는 방법
    • transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 추가해도 된다. → 기본 readObject 메서드 사용
    • 아니다. → 커스텀 readObject 메서드를 만들어 모든 유효성 검사와 방어적 복사 수행 혹은 직렬화 프록시 패턴(아이템 90) 사용
  • final 이 아닌 직렬화 가능 클래스라면 readObject와 생성자의 공통점이 하나 있다.
    • 마치 생성자처럼 readObject 메서드도 재정의 가능 메서드를 (직접적, 간접적) 호출해서는 안 된다.
    • 이 규칙을 어겼는데, 해당 메서드가 재정의되면, 하위 클래스의 상태가 완전히 역직렬화되기 전에 하위 클래스에서 재정의된 메서드가 실행되어 프로그램 오작동으로 이어진다.

 

 

 

정리

  • readObject 메서드를 작성할 때는 public 생성자를 작성하는 자세로 임해야 한다.
  • readObject는 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어야 한다.
  • 바이트 스트림이 진짜 직렬화된 인스턴스라고 가정해서는 안 된다.
  • 기본 직렬화 뿐 아니라 커스텀 직렬화도 모든 문제가 그대로 발생할 수 있다.
  • 안전한 readObject 메서드 작성 요령
    • private이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라. 불변 클래스 내의 가변 요소가 여기 속한다.
    • 모든 불변식을 검사하여 어긋나는 게 발견되면 InvalidObjectException을 던진다. 방어적 복사 다음에는 반드시 불변식 검사가 뒤따라야 한다.
    • 역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용하라
    • 직접적이든 간접적이든, 재정의할 수 있는 메서드는 호출하지 말자
반응형

'BE > Java' 카테고리의 다른 글

[Effective Java] 아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라  (0) 2023.01.05
[Effective Java] 아이템 89. 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라  (0) 2023.01.04
[Effective Java] 아이템 87. 커스텀 직렬화 형태를 고려해보라  (1) 2023.01.02
[Effective Java] 아이템 86. Serializable을 구현할지는 신중히 결정하라  (0) 2022.12.31
[Effective Java] 아이템 85. 자바 직렬화의 대안을 찾으라  (0) 2022.12.28
  1. 아이템 88. readObject 메서드는 방어적으로 작성하라
  2. readObject 메서드 Tip
  3. 정리
'BE/Java' 카테고리의 다른 글
  • [Effective Java] 아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
  • [Effective Java] 아이템 89. 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라
  • [Effective Java] 아이템 87. 커스텀 직렬화 형태를 고려해보라
  • [Effective Java] 아이템 86. Serializable을 구현할지는 신중히 결정하라
멍목
멍목
개발 관련 새롭게 알게 된 지식이나 좋은 정보들을 메모하는 공간입니다.
반응형
멍목
김멍목의 개발블로그
멍목
전체
오늘
어제
  • 분류 전체보기 (514)
    • BE (190)
      • Spring (21)
      • Java (141)
      • Kotlin (6)
      • JPA (22)
    • FE (33)
      • Javascript (16)
      • Typescript (0)
      • React (5)
      • Vue.js (9)
      • JSP & JSTL (3)
    • DB (32)
      • Oracle (22)
      • MongoDB (10)
    • Algorithm (195)
    • Linux (8)
    • Git (6)
    • etc (42)
    • ---------------------------.. (0)
    • 회계 (4)
      • 전산회계 2급 (4)
    • 잡동사니 (2)

블로그 메뉴

  • 홈
  • 관리

공지사항

인기 글

태그

  • 자바 공부
  • 자기 공부
  • MongoDB 공부
  • 자기공부
  • Java to Kotlin
  • 프로젝트로 배우는 Vue.js 3
  • Oracle
  • java 8
  • MongoDB with Node.js
  • 알고리즘공부
  • JPA
  • 자바 개발자를 위한 코틀린 입문
  • Effective Java
  • 자기 개발
  • junit5
  • 자바공부
  • vue3 공부
  • 코테공부
  • 코틀린
  • 이펙티브자바
  • 자바 테스팅 프레임워크
  • 더 자바 애플리케이션을 테스트하는 다양한 방법
  • 자기개발
  • JPA 공부
  • MongoDB 기초부터 실무까지
  • 코테 공부
  • 전산회계 2급 준비
  • 알고리즘 공부
  • 이펙티브 자바
  • 더 자바 Java 8

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
멍목
[Effective Java] 아이템 88. readObject 메서드는 방어적으로 작성하라
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.