CodeGym /행동 /JAVA 25 SELF /직렬화 보안: 모범 사례

직렬화 보안: 모범 사례

JAVA 25 SELF
레벨 43 , 레슨 4
사용 가능

1. 안전한 직렬화를 위한 핵심 모범 사례

직렬화 — 이는 공항에서 수하물을 포장하는 것과 비슷합니다: 내부에 무엇이 있는지, 그리고 가방을 누구에게 맡기는지 모른다면 보안 검사에서 불쾌한 놀라움을 맞을 수 있습니다. Java의 직렬화는 객체를 쉽게 저장하고 복원할 수 있게 하지만, 데이터가 신뢰할 수 없는 소스에서 들어올 경우 다양한 공격의 문을 열어 줍니다.

전형적인 위협:

Java의 직렬화는 안전하지 않을 수 있습니다. 공격자가 악성 스트림을 주입하면, 역직렬화 시 필드 변경부터 원치 않는 코드 실행까지 가장 바람직하지 않은 결과가 발생할 수 있습니다. 이는 교과서 속 공포 이야기가 아닙니다 — 실제로 Java 역사에는 바로 이 메커니즘을 악용한 공격 사례가 존재했습니다.

왜 이런 일이 발생할까요?

사실 역직렬화 — 이는 단순히 필드 값을 복원하는 행위가 아닙니다. 과정에서 완전한 객체가 생성되며, 특별한 메서드(예: readObject, readResolve)가 호출될 수 있고, 때로는 리플렉션을 통해 코드의 취약 지점이 노출되기도 합니다. 특히 서드파티 라이브러리의 클래스가 위험합니다: 일부는 역직렬화 단계에서 이미 동작을 수행합니다. 따라서 외부에서 받은 직렬화된 데이터를 절대 신뢰하지 마십시오.

transient를 민감한 데이터에 사용하세요

클래스에 비밀번호, 토큰, 개인 키 같은 민감한 정보를 담는 필드가 있다면, 해당 필드는 transient로 선언하세요. 이러한 데이터는 직렬화 스트림에 포함되지 않습니다.

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // 직렬화되지 않음

    // ...생성자, getter, setter...
}

역직렬화 시 무엇이 일어날까요? password 필드는 기본값을 갖게 됩니다(문자열의 경우 null). 이는 좋은 일입니다: 비밀번호가 파일에 저장되거나 네트워크로 전송되지 않습니다.

serialVersionUID를 명시적으로 정의하세요

항상 serialVersionUID를 명시적으로 지정하세요. 이는 호환성 오류의 가능성을 낮추고, 역직렬화 시 클래스가 바뀌는 위험을 최소화합니다.

private static final long serialVersionUID = 1L;

왜 보안에 중요한가요? serialVersionUID를 지정하지 않으면, 컴파일러가 클래스 구조를 기반으로 자동 생성합니다. 이는 예기치 않은 불일치를 유발할 수 있고, 이론상 동일한 이름이지만 구조가 다른 클래스로 바꾸는 악용 가능성을 키울 수 있습니다.

역직렬화 시 객체 타입을 검증하세요

네트워크나 파일에서 온 데이터를 신뢰하지 마십시오. 역직렬화 이후에는 작업하기 전에 항상 수신 객체가 기대한 타입인지 확인해야 합니다.

Object obj = objectInputStream.readObject();
if (obj instanceof User) {
    User user = (User) obj;
    // user를 안전하게 사용
} else {
    // 예상치 못한 타입 — 예외를 던지거나 오류 처리
}

왜 필요한가요? 악성 스트림에는 Serializable을 구현하지만 비즈니스 로직에 맞지 않는 다른 클래스의 객체가 들어 있을 수 있습니다.

역직렬화 가능한 클래스를 제한하세요 (ObjectInputFilter)

Java 9부터는 ObjectInputFilter 같은 필터를 사용해 역직렬화가 허용되는 클래스 집합을 제한하세요. 이는 출입 통제와 같습니다.

예시: 필터 설정

import java.io.*;

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;com.example.Address;!*"
);

ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);

Object obj = in.readObject(); // 이제 User와 Address만 역직렬화됨

이 필터는 애플리케이션의 UserAddress 클래스만 허용합니다. 나머지는 차단되어 예외가 발생합니다. 이는 악성 객체가 들어올 위험을 크게 낮춥니다.

신뢰할 수 없는 소스의 데이터를 역직렬화하지 마세요

황금률: 데이터 출처를 확신할 수 없다면 — 역직렬화하지 마세요. 파싱 시 코드를 실행하지 않는 포맷(예: JSON, 안전한 파서가 적용된 XML)을 선호하세요.

나쁜 관행의 예:

// 인터넷에서 온 데이터로 이렇게 하지 마세요!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // 위험함!

더 나은 방법은?

  • JSON 파서(예: Gson/Jackson) 또는 검증 기능이 있는 XML 파서를 사용하세요.
  • 바이너리 직렬화가 꼭 필요하다면 — ObjectInputFilter로 클래스를 필터링하고 instanceof로 타입을 검증하세요.

외부 시스템과의 교환에는 대체 포맷을 사용하세요

통합에서는 파싱 시 코드를 실행하지 않는 포맷: JSON, XML, Protocol Buffers 등을 사용하세요. 이는 역직렬화 기반 공격을 거의 차단합니다.

// ObjectInputStream 대신 JSON 파서를 사용하세요
User user = gson.fromJson(jsonString, User.class);

직렬화된 객체를 공개 위치에 저장하지 마세요

직렬화된 객체가 담긴 파일에는 민감한 데이터가 포함될 수 있습니다. 공용 디렉터리에 저장하지 말고 파일 시스템 수준에서 접근 권한을 제한하세요.

무결성 검증에 직렬화를 의존하지 마세요

직렬화는 데이터의 무결성이나 진위를 보장하지 않습니다. 변경이 허용되지 않는다면 디지털 서명, 체크섬 또는 암호화를 사용하세요.

2. 실습: ObjectInputFilter 예제와 취약점 데모

클래스 필터링 예제

다음과 같은 User 클래스가 있다고 가정해봅시다:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password;

    // ...생성자, getter, setter...
}

필터는 오직 User만 허용합니다:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;!*"
);
in.setObjectInputFilter(filter);

이제 누군가 다른 클래스의 객체를 주입하려 하면, 역직렬화는 오류로 종료됩니다.

잠재적 취약점 시연

악성 클래스:

// 누군가 이런 클래스를 끼워 넣었다고 가정해 봅시다
public class Evil implements java.io.Serializable {
    static {
        System.out.println("악성 코드가 실행되었습니다!");
        // 여기에는 무엇이든 있을 수 있음...
    }
}

클래스를 필터링하지 않으면, 역직렬화 시 Evil 객체가 생성될 수 있고, 클래스 로딩 시 정적 이니셜라이저가 실행됩니다 — 이는 실제 공격 시나리오입니다.

4. 직렬화 보안을 망치는 대표적인 실수

오류 №1: 필터링과 타입 검증 없이 역직렬화함. 개발자는 종종 스트림에서 객체를 읽자마자 원하는 타입으로 캐스팅합니다. 이는 공격의 문을 엽니다. ObjectInputFilter를 사용하고 instanceof로 타입을 검증하세요.

오류 №2: transient 없이 민감한 데이터를 저장함. 비밀번호/키를 transient로 선언하지 않으면, 그 값이 스트림에 포함되어 파일과 함께 유출될 수 있습니다.

오류 №3: serialVersionUID 없음. 명시적인 serialVersionUID가 없으면 예기치 않은 호환성 오류와 클래스 바꾸기와 관련된 위험이 발생할 수 있습니다.

오류 №4: 외부 시스템과의 교환에 직렬화를 사용함. 바이너리 직렬화는 애플리케이션 내부(예: 캐시)에서는 편리하지만, 외부 교환에는 위험합니다. 안전한 파서를 갖춘 JSON/XML/Proto를 선호하세요.

오류 №5: 데이터 무결성을 무시함. 직렬화된 파일의 바이트가 변경되어도 눈치채기 어렵습니다. 디지털 서명, 체크섬 또는 암호화를 적용하세요.

1
과제
JAVA 25 SELF, 레벨 43, 레슨 4
잠금
마법사의 유물: 보물을 위한 버전 태그
마법사의 유물: 보물을 위한 버전 태그
1
과제
JAVA 25 SELF, 레벨 43, 레슨 4
잠금
CRM 고객: 로드 시 데이터 무결성 보장
CRM 고객: 로드 시 데이터 무결성 보장
1
설문조사/퀴즈
시리얼라이제이션 설정, 레벨 43, 레슨 4
사용 불가능
시리얼라이제이션 설정
시리얼라이제이션 설정
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION