본문 바로가기
Language/Effective java

직렬화

by y.j 2022. 9. 3.
728x90

자바 직렬화의 대안을 찾으라.

자바의 역직렬화는 명백하고 현존하는 위험이다. 이 기술은 지금도 애플리케이션에서 직접 혹은,
자바 하부시스템(RMI, JMX, JMS)을 통해 간접적으로 쓰이고 있기 때문이다.
신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(RCE), 서비스 거부(DoS)등의 공격으로 이어질 수 있다. 잘못한게 아무것도 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다.

 

직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다. ObjectInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다. readObject는 거의 모든 타입의 코드를 수행 할 수 있다. 

 

가젯(써드파티 라이브러리)을 통해 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 찾을 수 있지만 네이티브 코드를 마음대로 실행 할 수 있는 가젯체인도 발견된다.

 

오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출된다.

  • 스트림의 전체 크기는 5744바이트지만 역지렬화는 끝나지 않는다.
  • HashSet인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 한다.
  • 해시코드를 계산하는데 $2^{100}$번 넘게 호출해야한다.
  • 무언가가 잘못되었는지 신호도 주지 않는다.
  • 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.
    Set<Object> root = new HashSet<>();
    Set<Object> s1 = root;
    Set<Object> s2 = new HashSet<>();
    
    for(int i = 0; i < 100; i++) {
        Set<Object> t1 = new HashSet<>();
        Set<Object> t2 = new HashSet<>();
        t1.add("foo");
        s1.add(t1); s1.add(t2);
        s2.add(t1); s2.add(t2);
        s1 = t1;
        s2 = t2;
    }
    return serialize(root);
}

 

 

Cross-platform structured-data representation

  • 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘
  • 자바 직렬화보다 훨씬 간단하고 임의 객체 그래프를 자동으로 직렬화/역직렬화하지 않는다.
  • 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 제공한다.
  • 예 : JSON

레거시 시스템 때문에 자바 직렬화를 완전히 배제할 수 없을 때는 역직렬화하지 않는것이다. RMI는 절대 수용해서는 안된다. 

 

직렬화를 피할 수 없고 역직렬화한 데이터가 안전한지 완전히 확신 할 수 없다면 객체 역직렬화 필터링을 사용하자.

  • 특정 클래스를 받아들이거나 거부할 수 있다.
  • '기본 수용'모드에서는 블랙리스트에 기록된 잠재적 위험한 클래스들을 거부할 수 있다.
  • '기본 거부'모드에서는 화이트스트레 기록된 클래스들만 수용한다.
  • 블랙리스트 방식보다는 화이트리스트 방식을 추천한다.

 

Serializable을 구현할지는 신중히 결정하라.

Seriablizable을 구현하면 릴리즈한 뒤에는 수정하기 어렵다. 한번 배포하게 되면 영원히 지원해야 한다.

 

Serializable구현하면 릴리스한 뒤에는 수정하기 어렵다.

  • 캡슐화가 깨진다.
  • 직렬화 내부 구현을 바꿀 경우 원래의 직렬화 형태와 달라지게 된다.
  • 잘 설계하더라도 클래스를 개선하는 데 제약이 될 수 있다.

 

버그와 보안 구멍이 생길 위험이 높아진다. 

  • 객체 생성하는 우회적인 방법이 생기므로 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.

 

해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다는 점이다.

  • 직렬화/역직렬화를 모두 성공해야 한다.
  • 충실히 복제해내는지를 반드시 확인해야 한다.
  • 신버전 인스턴스 직렬화한 후 구버전으로 역직렬화할 수 있는지 그 반대도 확인해야 한다.

 

Serializable 구현 여부는 가볍게 결정할 사안이 아니다.

  • 비용이 많이 드므로 이득과 비용을 잘 저울질 해야 한다.
  • 역사적으로 BigInteger와 Instant같은 '값'클래스와 컬렉션 클래스들은 Serializable을 구현하고 스레드 풀처럼 '동작'하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않았다.

 

상속용으로 설계된 클래스와 인터페이스 대부분 Serializable을 구현하면 안된다.

  • 클래스를 확장하거나 구현하는 이에게 너무 큰 부담을 지우게 된다.
  • finalizer메서드를 자신이 재정의를 못하게 하거나 final로 선언하여 finalizer공격에 대비해야 한다.
  • Serializable을 사용하지 않을 때는 하위 클래스도 사용하지 않는다는 것을 고려해야 한다.
  • 내부 클래스는 직렬화를 구현하지 말아야 한다.
  • 단, 정적 멤버 클래스는 Serializable을 구현해도 된다.

 

 

 

728x90

'Language > Effective java' 카테고리의 다른 글

직렬화  (0) 2022.09.04
직렬화  (0) 2022.09.03
동시성  (0) 2022.09.02
동시성  (0) 2022.08.27
동시성  (0) 2022.08.24

댓글