직렬화가 품고 있는 위험


직렬화 관련 게시글



85. 자바 직렬화의 대안

직렬화의 근본적인 문제는 공격 범위가 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다.

ObjectInputStreamreadObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다.

readObject 메서드는 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있는 생성자다.

바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다.

즉 그 타입들의 코드 전체가 공격 범위에 들어간다는 뜻이다.

역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들이 있고 (이를 가젯이라고 부른다.)

가젯을 함께 사용하여 가젯 체인을 구성해 공격할 수도 있다.

또한 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다.

이런 스트림을 역직렬화 폭탄이라고 한다.


이 객체 그래프는 201개의 HashSet 인스턴스로 구성되며, 각각 3개 이하의 객체 참조를 갖는다.

스트림의 전체 크기는 5744바이트지만 역직렬화는 끝나지 않을 것이다.


문제는 HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 하는 것이다.

반복문에 의해 이 구조의 깊이는 100단계까지 만들어지고 이 HashSet을 역직렬화하려면

hashCode 메서드를 2^100번 넘게 호출해야 한다.

역직렬화가 끝나지 않는 것도 문제고 잘못되었다는 신호조차 주지 않는 것도 문제다.

이 코드는 몇 개의 객체만 생성해도 스택 깊이 제한에 걸린다.



86. Serializable을 구현할지 신중히 결정하자

직렬화할 수 있게 하려면 implements Serializable만 덧붙이면 된다.

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

클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다.

기본 직렬화 상태에서는 클래스의 privatepackage-private 인스턴스 필드들마저

API로 공개되는 것이다. (캡슐화가 깨진다.)



문제1: SerialVersionUID

직렬화가 클래스 개선을 방해하는 예는 대표적으로 스트림 고유 식별자, 즉 직렬 버전 UID를 들 수 있다.

모든 직렬화된 클래스는 고유 식별 번호를 부여받는다.

이 번호를 명시하지 않으면 시스템이 런타임에 암호 해시 함수를 적용해 자동으로 클래스 안에 생성해 넣는다.

이 값을 생성하는 데는 클래스 이름, 구현 인터페이스, 컴파일러가 자동으로 넣은 클래스 멤버들이 고려된다.

그래서 나중에 이들 중 하나라도 수정한다면 직렬 버전 UID 값도 변한다.

즉 자동 생성되는 값에 의존하면 쉽게 호환성이 깨져버려 런타임에 InvalidClassException이 발생할 것이다.



문제2: 역직렬화는 숨은 생성자다.

두 번째 문제는 버그와 보안 구멍이 생길 위험이 높아진다는 점이다.

객체는 생성자를 사용해 만드는 게 기본인데 직렬화는 우회하는 객체 생성 기법인 것이다.

역직렬화는 일반 생성자의 문제가 그대로 적용되는 숨은 생성자다.

기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다는 뜻이다.



문제3: 신버전 릴리스할 때 테스트 추가

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

직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지

그 반대도 가능한지 검사해야 한다.



주의1: 상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며, 인터페이스도 대부분 Serializable을 확장해서는 안 된다.

이 규칙을 따르지 않으면, 그런 클래스를 확장하거나 그런 인터페이스를 구현하는 이에게 부담을 주게 된다.

예) 상속용으로 설계된 클래스 중 Serializable을 구현한 예로 ThrowableComponent가 있다.



주의2: 인스턴스 필드 값 중 불변식을 보장해야 할 게 있다면 반드시 하위 클래스에서 finaliza 메서드를 재정의하지 못하게 해야 한다.

finalize 메서드를 자신이 재정의하면서 final로 선언하면 된다.

이렇게 하지 않으면 finalizer 공격을 당할 수 있다.



주의3: 인스턴스 필드 중 기본값(정수 0, boolean false, 객체 null)으로 초기화되면 위배되는 불변식이 있다면 readObjectNoData 메서드 추가

기존의 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 추가하는 드문 경우를 위한 메서드다.



주의4: 내부 클래스는 직렬화를 구현하지 말아야 한다.

내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다.

내부 클래스에 대한 기본 직렬화 형태는 분명하지가 않다.

단, 정적 멤버 클래스는 Serializable을 구현해도 된다.