flawopen.com/XML 외부 엔티티(XXE)/Python XML 파서 보안
Python xml.etree.ElementTree는 XXE에 안전한가요?
파일 읽기나 네트워크를 이용하는 전형적인 XXE에 대해 Python 내장 Expat 파서는 기본적으로 로컬 파일에 접근하거나 네트워크 연결을 열지 않습니다. 하지만 모든 XML 처리가 안전한 것은 아닙니다. Expat 2.7.2 미만은 엔티티 확장과 긴 토큰으로 DoS가 발생할 수 있습니다. lxml 기본값도 버전에 따라 다릅니다. 파서 버전을 고정하고 옵션을 명시하거나 신뢰할 수 없는 XML에 defusedxml을 사용하세요.
위험 — 버전과 옵션이 노출 범위를 바꿈
- XXE (CWE-611):
- 파서가 공격자 제어 XML의 외부 엔티티를 확인해 로컬 파일을 읽거나 서버 요청을 보낼 수 있습니다.
- 엔티티 확장 DoS:
- 중첩되거나 반복되는 참조가 확장되며 CPU 또는 메모리를 과도하게 사용합니다.
- Expat:
- Python 내장 XML 모듈이 사용하는 C 파서입니다. 실제 버전은
pyexpat.EXPAT_VERSION으로 확인하세요. - DTD:
- 문서 유형 선언이 엔티티를 정의하고 설정에 따라 외부 리소스를 요청할 수 있습니다.
신뢰할 수 없는 XML이 파서에 도달
업로드, SVG, SAML 메시지, 연동 데이터가 신뢰 경계를 넘어 XML API에 전달됩니다.
구현과 옵션이 동작을 결정
표준 라이브러리는 Expat, lxml은 libxml2를 사용합니다. 버전, 외부 확인, 확장, 플래그가 가능한 동작을 정합니다.
확인 또는 확장이 영향을 줌
외부 확인이 켜져 있으면 엔티티가 로컬·원격 리소스를 요청할 수 있습니다. 취약한 확장이나 긴 토큰도 자원을 소진시킬 수 있습니다.
프로세스 권한이 영향을 제한
파일 권한과 네트워크 egress가 XXE 영향을 제한합니다. 입력 제한, 파서 업데이트, 안전 설정, 격리로 노출을 줄이세요.
현재 Python 보안 지침에 따르면 Expat은 기본적으로 로컬 파일이나 네트워크에 접근하지 않습니다. 다만 2.7.2 미만 버전은 billion laughs, quadratic blowup, 긴 토큰, 과도한 메모리 사용에 취약할 수 있습니다. Python이 번들 또는 시스템 Expat을 사용할 수 있으므로 pyexpat.EXPAT_VERSION을 확인하세요. 이는 버전별 리소스 고갈 위험이며, 현재 ElementTree가 외부 엔티티를 확인한다는 뜻은 아닙니다.
# Standard-library external access is off by default, # but old Expat builds have resource-exhaustion risks. import xml.etree.ElementTree as ET from xml.parsers import expat print(expat.EXPAT_VERSION) # inspect the runtime parser root = ET.fromstring(untrusted_xml) # lxml before 5.0 defaulted resolve_entities=True. # Explicit unsafe settings illustrate the risky boundary. from lxml import etree parser = etree.XMLParser(resolve_entities=True, load_dtd=True) root = etree.fromstring(untrusted_xml, parser)
from defusedxml.ElementTree import fromstring
# Reject DTD declarations when the format does not require them.
root = fromstring(untrusted_xml, forbid_dtd=True)
# If lxml is required, set its security options explicitly.
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False,
no_network=True,
load_dtd=False,
huge_tree=False,
)
root = etree.fromstring(untrusted_xml, parser)
# Do not run XInclude on untrusted trees without a restricted loader.
실제 배포 파서에 맞는 안전 설정 선택
defusedxml.ElementTree는 엔티티 선언과 외부 참조를 기본 차단합니다. DTD가 필요 없는 형식이면 forbid_dtd=True도 지정하세요. lxml은 resolve_entities=False, load_dtd=False, no_network=True, huge_tree=False를 명시합니다. 현재 버전은 내부 엔티티만 기본 확인하고 lxml 5.0 이전은 기본 확인이 활성화되어 있었습니다. 제한된 loader 없이 신뢰할 수 없는 트리에 XInclude를 실행하지 마세요. 파싱과 압축 해제 전에 크기를 제한하세요.
- ✓모든 XML 입력점과 파서를 목록화하세요: ElementTree/Expat, lxml/libxml2, SAX/DOM, SVG, SAML, 아카이브 가져오기, 후속 XInclude 처리.
- ✓가능하면 Expat 2.7.2 이상이 포함된 지원 Python을 사용하고
pyexpat.EXPAT_VERSION으로 실행 버전을 확인하세요. - ✓신뢰할 수 없는 XML에는
defusedxml을 사용하고 DTD가 불필요하면forbid_dtd=True로 거부하세요. - ✓lxml은 유지보수 버전으로 고정하고 엔티티, DTD 로드, 네트워크, huge-tree를 명시적으로 비활성화하세요. 변하는 기본값에 의존하지 마세요.
- ✓압축 전후 크기를 제한하고 필요하면 파싱을 격리하며 외부 요청 없이 무해한 DTD/entity fixture를 CI에서 테스트하세요.
파서 기본값을 버전별로 검토해야 하는 이유
“Python XML”은 단일 설정이 아닙니다. 표준 라이브러리는 빌드에 따라 번들이나 시스템 Expat을 사용합니다. lxml은 libxml2를 감싸며 5.0에서 엔티티 기본값을 바꿨고 wrapper와 명시적 설정도 동작을 바꿀 수 있습니다. Python, Expat, lxml, libxml2 버전을 기록하고 신뢰할 수 없는 입력을 받는 각 지점에 옵션을 명시하세요.
자주 묻는 질문
아니요. Python 문서에 따르면 Expat은 기본적으로 로컬 파일이나 네트워크를 이용하지 않습니다. 버전을 확인하고 XXE 파일 접근과 오래된 파서의 자원 고갈을 구별하세요.
버전을 구분해야 합니다. 현재 XMLParser는 내부 엔티티만 확인하고 DTD와 네트워크를 기본 사용하지 않습니다. lxml 5.0에서 resolve_entities 기본값이 True에서 internal로 바뀌었습니다. 옵션과 wrapper를 확인하세요.
아니요. 엔티티와 외부 참조는 차단하지만 forbid_dtd는 False입니다. DTD가 불필요하면 True로 설정하세요.
아니요. 압축 및 해제 후 크기를 제한하고 파서를 업데이트하며 huge_tree를 피하세요. 제한 없는 loader로 신뢰할 수 없는 XInclude를 처리하지 마세요.