설명
역직렬화는 저장하거나 전송한 표현을 애플리케이션 객체로 복원하는 과정입니다. Python의 역직렬화 방식은 다음과 같이 서로 다른 보안 특성을 가집니다.
pickle.load,pickle.loads와Unpickler.load()는 객체를 복원하면서 전역 함수나 클래스를 찾고 호출할 수 있습니다. Python은pickle을 안전하지 않은 모듈로 명시하며 신뢰할 수 있는 데이터만 언피클링하도록 안내합니다. 악성 pickle은 언피클링 도중 임의 코드를 실행할 수 있습니다.- PyYAML의
yaml.unsafe_load와Loader,CLoader,UnsafeLoader,CUnsafeLoader는 Python 객체를 구성하는 태그를 허용합니다. 신뢰되지 않은 YAML에는 이러한 로더를 사용하면 안 됩니다. marshal은 주로 Python의.pyc지원을 위한 내부 형식입니다. Python은 악의적으로 구성된 데이터에 안전하지 않다고 명시합니다.allow_code=True이면 코드 객체를 반환할 수 있지만, 언마샬링 자체가 그 코드 객체를 실행하는 것은 아닙니다. 손상된 입력, 버전이 맞지 않는 코드 객체 및 역직렬화 결과의 후속 사용은 예외, 정의되지 않은 동작 또는 추가 보안 문제를 일으킬 수 있습니다.
따라서 pickle 또는 안전하지 않은 PyYAML 로더의 코드 실행 위험과 marshal의 내부 형식·무결성·가용성 위험을 같은 동작으로 설명해서는 안 됩니다.
잠재적 영향
- 임의 코드 실행: 악성 pickle 또는 안전하지 않은 PyYAML Python 태그가 애플리케이션 권한으로 함수나 명령을 실행할 수 있습니다.
- 데이터 및 상태 변조: 예상하지 않은 객체나 값이 생성되어 애플리케이션 상태와 저장 데이터를 변경할 수 있습니다.
- 정보 노출: 실행된 코드나 후속 처리 로직이 파일, 비밀 값 또는 환경 정보에 접근할 수 있습니다.
- 서비스 거부: 악의적이거나 과도하게 큰 입력이 CPU, 메모리 또는 재귀 한도를 소모하거나 반복적인 예외를 유발할 수 있습니다.
- 호환성 및 안정성 문제:
marshal코드 객체는 Python 버전 간 호환되지 않으며, 잘못된 버전에서 역직렬화하면 정의되지 않은 동작이 발생할 수 있습니다.
역직렬화 형식 선택
신뢰할 수 없는 입력에는 JSON이나 yaml.safe_load처럼 데이터만 표현하는 형식을 우선 사용하세요. Unpickler를 변수에 저장해 호출하거나 사용자 정의 로더를 사용해도 입력의 신뢰성을 검증할 필요는 동일합니다.
해결 방법
- 외부 입력 경로에서
pickle과marshal을 제거하고 JSON처럼 데이터만 표현하는 형식으로 교체합니다. - 입력 크기를 제한하고 파싱 결과의 객체 종류, 필수 필드, 값의 타입·길이·범위를 검증합니다. 스키마 검증은 데이터 전용 파서 뒤에 수행해야 하며, 안전하지 않은 역직렬화 뒤의 검증은 코드 실행을 막지 못합니다.
- 신뢰되지 않은 YAML에는
yaml.safe_load또는yaml.load(..., Loader=yaml.SafeLoader)를 사용합니다. 사용 가능한 환경에서는CSafeLoader도 같은 제한된 생성자 집합을 사용합니다. - 레거시 객체 형식을 즉시 제거할 수 없다면 신뢰 경계를 바꿔 인증된 신뢰 가능한 생성 주체만 데이터를 만들게 하고, 역직렬화 전에 정확한 바이트의 진위성과 무결성을 검증합니다. HMAC은 비밀 키를 모르는 공격자의 변조를 막을 수 있지만 악의적인 권한 보유 생성 주체의 페이로드를 안전하게 만들지는 않습니다.
marshal.loads(..., allow_code=False), HTTPContent-Type, 인증·인가만 수행하는 방식, 역직렬화 이후의 스키마 검사 또는 허용 범위가 넓은 사용자 정의Unpickler를 단독 해결책으로 사용하지 않습니다.- 크기, 시간 및 메모리 제한은 가용성 방어로 추가하되, 신뢰 경계에서 안전하지 않은 역직렬화 함수를 제거하는 조치를 대신하게 해서는 안 됩니다.
예시
변경 전
python
import pickle
from flask import Flask, request
app = Flask(__name__)
@app.post("/profile/import")
def import_profile():
raw = request.get_data(cache=False)
profile = pickle.loads(raw) # 악성 pickle은 이 호출 중 코드를 실행할 수 있음
return {"name": str(profile.get("name", ""))}
요청 본문을 검사하기 전에 pickle.loads가 실행되므로, 반환된 객체를 검증해도 언피클링 중 발생한 코드 실행을 되돌릴 수 없습니다.
변경 후
외부 API가 JSON만 받도록 계약을 변경하고 파싱 후 스키마를 검증합니다.
python
from flask import Flask, abort, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024
@app.post("/profile/import")
def import_profile():
profile = request.get_json()
if not isinstance(profile, dict):
abort(400, "profile must be an object")
name = profile.get("name")
if not isinstance(name, str) or not 1 <= len(name) <= 100:
abort(400, "invalid name")
return {"name": name}
YAML이 반드시 필요하면 안전 로더로 기본 값만 만든 뒤 동일하게 스키마를 검증합니다.
python
import yaml
from flask import abort, request
def read_yaml_profile():
profile = yaml.safe_load(request.get_data(cache=False))
if not isinstance(profile, dict):
abort(400, "profile must be a mapping")
return profile
yaml.safe_load도 입력 크기와 애플리케이션 스키마를 대신 검증하지는 않습니다. 크기 제한과 파싱 후 검증을 함께 적용해야 합니다.
참조
- Python 3.14
pickle문서 - Python 3.14
marshal문서 - PyYAML 6.0.3 릴리스
- PyYAML 6.0.3 로딩 API 소스
- Flask 3.1.x
Request.get_jsonAPI - Flask 3.1.x
MAX_CONTENT_LENGTH설정 - OWASP ASVS 5.0.0 V1.5 Safe Deserialization
- OWASP Deserialization Cheat Sheet
- OWASP Top 10:2025 A08 Software or Data Integrity Failures
- OWASP Top 10:2021 A08 Software and Data Integrity Failures
- CWE-502: Deserialization of Untrusted Data