Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
letmeloveyou82 opened this issue Sep 12, 2023 · 0 comments
Open

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

letmeloveyou82 opened this issue Sep 12, 2023 · 0 comments
Assignees

Comments

@letmeloveyou82
Copy link
Member

letmeloveyou82 commented Sep 12, 2023

12장 주제 : 객체 직렬화

객체 직렬화

  • 자바가 객체를 바이트 스트림으로 인코딩하고(직렬화) 그 바이트 스트림으로부터 다시 객체를 재구성하는(역직렬화) 메커니즘
  • 직렬화된 객체는 다른 VM에 전송하거나 디스크에 저장한 후 나중에 역직렬화 가능

🧐 12장에서 집중하는 내용 : 직렬화의 위험성, 그 위험을 최소화하는 방법

미리 읽는 결론

직렬화는 위험하니 피하자. 시스템 밑바닥부터 설계한다면, JSON이나 프로토콜 버퍼 같은 대안을 사용하자. 신뢰할 수 없는 데이터는 역직렬화하지 말자. 꼭 해야 한다면 객체 역직렬화 필터링을 사용하되, 이마저도 모든 공격을 막아줄 수 없음을 기억하자. 클래스가 직렬화를 지원하도록 만들지 말고, 꼭 그렇게 만들어야 한다면 정말 신경써서 작성해야 한다.

1997년 자바에 직렬화 도입과 그 위험성

직렬화는 프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 구호는 매력적이었다.

하지만 다음과 같은 위험성이 그 장점을 압도했다.

  • 보이지 않는 생성자, API와 구현 사이의 모호해진 경계, 잠재적인 정확성 문제, 성능, 보안, 유지보수성
  • 특히 보안 문제는 심각했다. 2016년 11월엔 샌프란시스코 시영 교통국이 랜섬웨어 공격을 받아 요금 징수 시스템이 이틀간 마비되는 사태를 겪기도 했다.
  • 직렬화의 근본적인 문제공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다. ObjectInputStreamreadObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다. readObject 메서드는 (Serializable 인터페이스를 구현했다면) 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있는, 사실상 마법 같은 생성자다. 바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다. 즉, 그 타입들의 코드 전체가 공격 범위에 들어간다는 뜻이다.
    • 자바의 표준 라이브러리, 아파치 커먼즈 컬렉션 같은 서드파티 라이브러리는 물론 애플리케이션 자신의 클래스들도 공격 범위에 포함된다. 모든 모범 사례를 따르고, 모든 직렬화 가능 클래스들을 공격에 대비하도록 작성한다 해도, 우리의 애플리케이션은 여전히 취약할 수 있다.

    • CERT 조정 센터의 기술 관리자인 로버트 시커트(Robert Seacord)는 다음과 같이 말했다.

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

  • 공격자와 보안 전문가들은 자바 라이브러리와 널리 쓰이는 서드파티 라이브러리에서 직렬화 가능 타입들을 연구하여 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 찾아보았다. 이런 메서드를 ‘가젯(gadget)’ 이라 부른다. 여러 가젯을 함께 사용해 가젯 체인을 구성할 수도 있는데, 가끔씩 공격자가 기반 하드웨어의 네이티브 코드를 마음대로 실행할 수 있는 아주 강력한 가젯 체인도 발견된다. 참고로 샌프란시스코 교통국을 마비시킨 공격이 정확히 이런 사례였다. 우리는 아주 신중하게 제작한 바이트 스트림만 역직렬화해야 한다.
  • 가젯까지 갈 것도 없이, 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다. 이런 스트림을 ‘역직렬화 폭탄(deserialization bomb)’ 이라고 한다. 바우터르 쿠카르츠(Wouter Coekaerts)가 HashSet과 문자열만 사용해 만든 예를 살펴보자.

역직렬화 폭탄 예시

public class DeserializationBomb {
    public static void main(String[] args) throws Exception {
        System.out.println(bomb().length);
        deserialize(bomb());
    }

    // 역직렬화 폭탄 - 이 스트림의 역직렬화는 영원히 계속된다.
    static byte[] bomb() {
        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"); // t1을 t2와 다르게 만든다.
            s1.add(t1);
            s1.add(t2);
            s2.add(t1);
            s2.add(t2);
            s1 = t1;
            s2 = t2;
        }
        return serialize(root); // 간결하게 하기 위해 이 메서드의 코드는 생략함
    }
}

이 객체 그래프는 201개의 HashSet 인스턴스로 구성되며, 그 각각은 3개 이하의 객체 참조를 갖는다. 스트림의 전체 크기는 5744바이트지만, 역직렬화는 태양이 불타 식을 때까지도 끝나지 않을 것이다.

문제점 : HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 한다

  • 루트 HashSet에 담긴 두 원소는 각각 (루트와 마찬가지로) 다른 HashSet 2개씩을 원소로 갖는 HashSet이다. 또한, 반복문에 의해 이 구조가 깊이 100단계까지 만들어진다. 따라서 이 HashSet을 역직렬화하려면 hashCode 메서드를 2^100번 넘게 호출해야 한다. 역직렬화가 영원히 계속된다는 것도 문제지만, 무언가 잘못되었다는 신호조차 주지 않는 것도 큰 문제다. 이 코드는 단 몇 개의 객체만 생성해도 스택 깊이 제한에 걸려버린다.

자바 직렬화의 대안

1. 직렬화 위험을 회피하는 가장 좋은 방법 : 아무것도 역직렬화하지 않는 것. 크로스-플랫폼 구조화된 데이터 표현을 사용하자.

우리가 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다.

  • 객체와 바이트 시퀀스 변환해주는 다른 메커니즘 많다. 이 방식들은 자바 직렬화의 여러 위험을 회피하면서 다양한 플랫폼 지원, 우수한 성능, 풍부한 지원 도구, 활발한 커뮤니티와 전문가 집단 등 수많은 이점까지 제공한다. 이런 메커니즘들도 직렬화 시스템이라 불리긴 하지만, 이 책에선 자바 직렬화와 구분하고자 ‘크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)’ 이라 한다.

크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)의 공통점

  1. 임의 객체 그래프를 자동으로 직렬화/역직렬화하지 않는 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
  2. 기본 타입 몇 개와 배열 타입만 지원한다.

이런 간단한 추상화만으로도 아주 강력한 분산 시스템을 구축하기에 충분하고, 자바 직렬화가 가져온 심각한 문제들을 회피할 수 있음이 밝혀졌다.

크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)의 선두주자 : JSON, 프로토콜 버퍼

JSON 프로토콜 버퍼
더글라스 크록퍼드(Douglas Crockford)가 브라우저와 서버의 통신용으로 설계 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계
자바스크립트용으로 만들어짐 C++용으로 만들어짐
텍스트 기반이라 사람이 읽을 수 있음, 텍스트 기반 표현엔 JSON이 아주 효과적 이진 표현이라 효율이 훨씬 높음, 사람이 읽을 수 있는 텍스트 표현(pbtxt)도 지원함
오직 데이터를 표현하는 데만 쓰임 문서를 위한 스키마(타입)를 제공하고 올바로 쓰도록 강요

2. 레거시 시스템 때문에 자바 직렬화를 완전히 배제할 수 없을 때의 차선책 : 신뢰할 수 없는 데이터는 절대 역직렬화하지 않기

특히 신뢰할 수 없는 발신원으로부터의 RMI는 절대 수용해선 안 된다.

자바의 공식 보안 코딩 지침에선 “신뢰할 수 없는 데이터의 역직렬화는 본질적으로 위험하므로 절대로 피해야 한다”라고 조언한다.

3. 직렬화를 피할 수 없고, 역직렬화한 데이터가 안전한지 완전히 확신할 수 없을 때의 방법 : 객체 역직렬화 필터링 사용하기

객체 역직렬화 필터링(java.io.ObjectInputFilter)

  • 자바 9에 추가되었고, 이전 버전에서도 쓸 수 있도록 이식됨

  • 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능

  • 클래스 단위로, 특정 클래스를 받아들이거나 거부 가능

    • ‘기본 수용’ 모드에서는 블랙리스트에 기록된 잠재적으로 위험한 클래스들을 거부함
    • ‘기본 거부’ 모드에서는 화이트리스트에 기록된 안전하다고 알려진 클래스들만 수용함

    블랙리스트 방식보다는 화이트리스트 방식을 추천한다. 블랙리스트 방식은 이미 알려진 위험으로부터만 보호할 수 있기 때문이다. 애플리케이션을 위한 화이트리스트를 자동으로 생성해주는 스왓(SWAT, Serial Whitelist Application Trainer)이라는 도구도 있으니 참고하자.

  • 필터링 기능은 메모리를 과하게 사용하거나 객체 그래프가 너무 깊어지는 사태로부터도 보호해줌. 하지만, 앞서 보여준 직렬화 폭탄은 걸러내지 못함.

마치며

직렬화는 여전히 자바 생태계 곳곳에 쓰이고 있다. 자바 직렬화를 사용하는 시스템을 관리해야 한다면 시간과 노력을 들여서라도 크로스-플랫폼 구조화된 데이터 표현으로 마이그레이션하는 것을 심각하게 고민해보길 바란다. 하지만, 현실적인 이유로 지금도 직렬화 가능 클래스를 작성하거나 유지보수해야 하는 사람들이라면 다음 아이템부터 주의깊게 살펴보자.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant