파이썬 공급망 보안을 위한 심층 방어(Defense in Depth) 구현 가이드
(bernat.tech)핵심 요약
파이썬 패키지 생태계를 겨냥한 공급망 공격에 대응하기 위해 단일 통제에 의존하지 않는 다계층 방어 전략이 요구된다. 본문은 Ruff의 S(Bandit) 규칙을 활용한 정적 취약점 차단, uv를 이용한 암호학적 해시 기반 의존성 고정, CI 환경에서의 pip-audit 및 SBOM(CycloneDX) 생성을 권장한다. 패키지 배포 시에는 장기 API 토큰 대신 OIDC 기반 Trusted Publishing을 도입하며, 신규 패키지 도입을 의도적으로 지연(Delayed Ingestion)시켜 악성 패키지에 대한 커뮤니티 검증 시간을 확보하는 실무적 파이프라인을 제시한다.
심층 분석
- 공급망 공격 벡터와 전이 의존성(Transitive Dependency): 현재 PyPI에는 74만 개 이상의 패키지가 존재하며 ctx 도메인 탈취, Ultralytics(YOLO) CI 스크립트 인젝션, GhostAction 토큰 탈취 등 대규모 보안 사고가 지속 발생하고 있다. 단일 패키지(예: Flask)를 설치하더라도 수많은 전이 의존성(예: MarkupSafe 등)이 동반 설치되므로, 개발자가 명시적으로 선언하지 않은 패키지들까지 거대한 공격 표면(Attack Surface)으로 작용한다.
- 자체 코드 내 취약점 사전 차단: 외부 패키지 이전에 내부 코드의 결함을 방지해야 한다. 하드코딩된 시크릿, 취약한 해시 알고리즘(MD5, SHA1), 타임아웃이 누락된 네트워크 요청(DoS 유발), 안전하지 않은 직렬화(pickle) 등은 코드 리뷰 과정에서 누락되기 쉽다. 이를 방지하기 위해 CI/CD 파이프라인 및 IDE에 Ruff의 Bandit 보안 규칙을 연동하여 자동 탐지 및 차단하는 것이 필수적이다.
-
의존성 고정 및 지연 도입(Delayed Ingestion): 의존성 설치 시 단순히 버전을 명시하는 것을 넘어, 패키지의 암호학적 해시를 고정하여 런타임 환경에서의 위변조를 차단해야 한다. 또한 신규 배포된 악성 패키지가 차단되기까지의 리스크를 줄이기 위해
uv의--exclude-newer플래그를 사용하거나, 사내 미러 저장소에 '7일 인제스천 대기열(Ingestion Queue)'을 두어 커뮤니티의 일차적 검증을 거친 패키지만 내부망에 유입되도록 통제하는 전략이 유효하다. - 안전한 패키지 배포: 오픈소스 메인테이너 및 패키지 발행자는 탈취 위험이 상존하는 정적 API 토큰을 폐기하고, Sigstore를 활용한 OIDC(OpenID Connect) 기반 Trusted Publishing으로 전환해야 한다. 이를 통해 소스 리포지토리와의 연결성을 암호학적으로 증명하는 Attestation이 자동으로 생성된다.
주요 코드 및 데이터
1. 전이 의존성(Transitive Dependencies) 확인
# 단일 패키지(Flask) 설치 시 딸려오는 보이지 않는 의존성 트리 확인
uv pip install flask
uv pip tree
# Output:
flask v3.1.0
├── blinker v1.9.0
├── click v8.1.8
├── itsdangerous v2.2.0
├── jinja2 v3.1.5
│ └── markupsafe v3.0.2
└── werkzeug v3.1.3
└── markupsafe v3.0.2
2. Ruff를 활용한 보안 룰셋(Bandit) 적용
# pyproject.toml 설정 예시
[tool.ruff]
line-length = 120
# E(Error), F(Pyflakes)와 함께 S(Bandit 보안 규칙) 활성화
lint.select = ["E", "F", "S"]
# Ruff 'S' 룰셋에 의해 자동 탐지되는 대표적인 취약점 패턴 예시
# FLAGGED: S301 - pickle.loads()는 임의 코드 실행 위험이 있음 (json.loads() 권장)
import pickle
data = pickle.loads(untrusted_input)
# FLAGGED: S608 - 문자열 포매팅을 통한 SQL 인젝션 취약점
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
# FLAGGED: S113 - 타임아웃이 설정되지 않은 외부 요청 (무한 대기 및 DoS 공격 노출)
import requests
response = requests.get("[https://api.example.com/data](https://api.example.com/data)")
3. 지연 도입(Delayed Ingestion)을 통한 신규 패키지 통제
# 특정 날짜 이전(예: 7일 전)에 배포된 패키지만 의존성으로 컴파일하여 초기 악성코드/오류 회피
uv pip compile --exclude-newer 2026-03-02 requirements.in -o requirements.txt
4. 조직 내 인제스천 통제(Ingestion Control) 파이프라인
| 처리 단계 | 시스템 구성요소 | 보안 목적 및 설명 |
|---|---|---|
| 유입 | PyPI ➜ Ingestion Queue | PyPI에 등록된 신규 패키지를 사내 시스템에 즉시 배포하지 않고 대기열로 반입 |
| 대기 | Wait (ex. 7일) | 패키지의 악성 여부 및 취약점을 커뮤니티와 보안 연구원들이 분석할 절대적 시간 확보 |
| 검증 | Security Scan ➜ Approved | 대기 기간 종료 후, 알려진 취약점(CVE) 및 악성코드 스캔을 통과한 경우에만 승인 |
| 배포 | Internal Mirror ➜ Developers | 검증이 완료된 패키지만 내부 미러 저장소에 캐싱하여 개발팀이 안전하게 사용하도록 허용 |