위험한 역직렬화(Unsafe-Deserialization) 삼총사: yaml.load, pickle, eval
요약
AI 어시스턴트가 생성한 코드에서 yaml.load, pickle, eval과 같은 위험한 역직렬화 함수가 빈번하게 발견되는 보안 취약점을 분석합니다. 프롬프트의 단순한 요구사항을 충족하기 위해 AI가 보안보다 편의성을 우선시하는 패턴을 경고합니다.
핵심 포인트
- AI는 프롬프트 충족을 위해 보안에 취약한 API를 선택하는 경향이 있음
- yaml.load 대신 yaml.safe_load를 사용하여 공격 표면 제거 필요
- pickle 모듈은 신뢰할 수 있는 데이터에만 사용해야 함
- 설정 파일이나 외부 입력값이 임의 코드 실행의 통로가 될 수 있음
AI 어시스턴트들은 더 단순한 API를 선택하곤 합니다. yaml.load, pickle.loads, 그리고 eval은 더 안전한 대응 API들보다 사용하기 간편하며, 안전한 입력값에 대해서는 잘 작동하지만 공격자가 제어하는 입력값에 대해서는 임의의 코드(arbitrary code)를 실행합니다. BrassCoders는 공개된 벤치마크 코퍼스 내의 현실적인 프롬프트로부터 생성된 AI 생성 파일인 config_loader.py에서 yaml.load를 발견했습니다. 동일한 프롬프트 완성 역학(prompt completion dynamic)은 AI가 생성한 코드에서 pickle과 eval 또한 만들어냅니다.
이 세 가지 모두에서 나타나는 패턴은 동일합니다. 더 단순한 API가 프롬프트를 충족시킨다는 점입니다. 위험한 동작은 프롬프트가 설명하지 않은 입력값에서 나타납니다.
config_loader.py 내의 yaml.load
BrassCoders는 config_loader.py가 Bandit의 B506 규칙(unsafe yaml.load)에 해당한다고 표시했습니다. 왜냐하면 이 파일이 yaml.safe_load가 외부 입력을 받을 수 있는 설정 파일에 적합한 선택임에도 불구하고 yaml.load(f, Loader=yaml.Loader)를 호출하기 때문입니다.
이 파일은 "YAML 파일에서 설정을 읽고 환경 변수가 이를 재정의할 수 있는 설정 로더(config loader)를 작성하라"는 프롬프트로부터 생성되었습니다. 모델의 구현은 다음과 같습니다:
def load_config(path):
with open(path) as f:
config = yaml.load(f, Loader=yaml.Loader)
...
yaml.Loader는 PyYAML의 전체 로더(full Loader)입니다. 이는 역직렬화(deserialization) 과정에서 코드를 실행하는 !!python/object/apply:subprocess.run과 같은 Python 전용 YAML 태그를 지원합니다. 다음과 같은 내용을 포함하는 YAML 파일은:
exploit: !!python/object/apply:subprocess.run
- ["rm", "-rf", "/tmp/important"]
yaml.load(f, Loader=yaml.Loader)에 의해 파싱될 때 subprocess.run(["rm", "-rf", "/tmp/important"])를 실행합니다. 설정 파일이 공격 표면(attack surface)이 되는 것입니다. 설정 파일 또는 해당 파일에 기록되는 모든 입력을 제어하는 공격자는 임의의 Python 코드를 실행할 수 있습니다.
yaml.safe_load는 기본 YAML 타입(문자열, 숫자, 리스트, 딕셔너리, 불리언, null)만 파싱함으로써 이러한 공격 표면을 제거합니다. 교체 방법은 단 한 단어면 충분합니다:
config = yaml.safe_load(f)
pickle.loads: 동일한 패턴, 더 높은 위험성
pickle.loads는 바이트 스트림 (byte stream)으로부터 임의의 Python 객체를 역직렬화 (deserialization)합니다. 바이트를 제어하는 공격자는 Python이 실행하는 내용을 제어할 수 있습니다. Python 표준 라이브러리 문서 (Python standard library documentation)에는 다음과 같이 명시되어 있습니다: "pickle 모듈은 안전하지 않습니다. 신뢰할 수 있는 데이터만 unpickle 하십시오."
AI 어시스턴트들은 프롬프트가 모델 상태 (model state), 캐시된 결과 (cached results), 세션 데이터 (session data)와 같은 Python 객체의 빠른 직렬화 (serialization)를 요청할 때 pickle을 사용합니다. 프롬프트는 공격자가 제어하는 입력값에 대해 설명하지 않으므로, 모델은 이에 대한 방어 기제를 갖추지 않습니다. data가 사용자 입력이나 네트워크 소켓 (network socket)에서 오는 경우 pickle.loads(data)는 코드 실행 취약점 (code execution vulnerability)이 됩니다.
BrassCoders의 12개 파일로 구성된 심어진 벤치마크 (12-file planted benchmark)에는 pickle.loads 탐지 항목 (Bandit B301)이 포함되어 있습니다. 이 규칙은 모든 pickle.loads, pickle.load, 또는 cPickle 호출 시 발생합니다. 사용자 제공 데이터에 대한 해결책은 완전히 다른 직렬화 형식 (serialization format)을 사용하는 것입니다. 구조화된 데이터의 경우 JSON을 사용하거나, 실행 가능한 객체를 인코딩할 수 없는 메시지 형식을 사용해야 합니다.
eval: 문자열로부터의 임의 코드 실행
eval은 문자열로부터 Python 표현식 (expression)을 실행합니다. Python 구문 (syntax)을 포함하는 모든 문자열은 eval에 전달될 때 코드가 됩니다. Bandit의 B307 규칙은 모든 eval() 호출을 잠재적으로 안전하지 않은 것으로 플래그 (flag)를 지정합니다.
AI 어시스턴트들은 프롬프트가 계산된 값을 지원하는 설정 파일, 계산기, 템플릿 엔진과 같은 동적 표현식 평가 (dynamic expression evaluation)를 요청할 때 eval을 사용합니다. user_input이 HTTP 요청, 양식 필드 (form field), 또는 환경 변수 (environment variable)에서 오는 경우 eval(user_input)은 코드 실행 취약점이 됩니다. 사용자가 "__import__('os').system('id')"를 전달하면 애플리케이션은 이를 실행하게 됩니다.
더 안전한 대안은 사용 사례에 따라 다릅니다: 리터럴 Python 값(문자열, 숫자, 리스트, 딕셔너리)만 생성해야 하는 표현식에는 ast.literal_eval을 사용하고, 도메인 특화 구문(domain-specific syntax)에는 전용 파서(parser)를 사용하거나, 구조화된 데이터 접근을 위해 쿼리 언어(query language)를 사용할 수 있습니다. 이들 중 그 어떤 것도 임의의 Python 코드를 실행하지 않습니다.
BrassCoders는 구조적 수준에서 eval을 탐지합니다. AI 분류(triage) 레이어는 함수 시그니처(function signature)와 호출 지점(call site)을 읽어 특정 호출의 입력값이 사용자에 의해 제어되는지 판단함으로써, 해당 탐지 결과가 실제 위협인지 결정합니다.
프롬프트 완성 패턴이 이 세 가지를 생성하는 이유
세 가지 API는 모두 동일한 속성을 공유합니다. 즉, 안전한 입력값에 대해서는 프롬프트의 요구사항을 충족하면서도 더 단순하고 표현력이 풍부한 옵션이라는 점입니다. yaml.safe_load 대신 yaml.load를, json.loads 대신 pickle.loads를, ast.literal_eval 대신 eval을 사용하는 식입니다. 더 단순한 API는 테스트 케이스에서는 잘 작동합니다. 하지만 위험한 동작은 프롬프트가 설명하지 않은 운영(production) 환경의 사례에서 발생합니다.
이것이 BrassCoders가 문맥을 추론하기보다 구조적으로 이들을 탐지하는 이유입니다. 만약 파일 이름이 config_loader.py라고 해서 yaml.load 탐지 결과의 등급을 낮추는 규칙이 있다면, 구성 로더(configuration loader)가 외부 입력에 노출되는 사례를 놓치게 될 것입니다. 이는 바로 프롬프트가 설명하고 있는 상황입니다. BrassCoders는 패턴을 보고하고, Claude Code는 호출 지점을 읽어 입력값이 신뢰할 수 있는지를 결정합니다.
config_loader.py 탐지 결과 재현하기
git clone https://github.com/CopperSunDev/brasscoders
pip install brasscoders
brasscoders --offline scan brasscoders/docs/benchmarks/ai-code-findings-corpus/files
config_loader.py에 대한 B506 탐지 결과는 .brass/ai_instructions.yaml 파일에 심각도 high, 파일 경로, 줄 번호, 그리고 yaml.safe_load를 통한 수정 권고(remediation)와 함께 나타납니다. 매 실행 시 동일한 출력이 생성됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기