FlawOpen

flawopen.com/XML 외부 엔티티(XXE)/Python XML 파서 보안

Python xml.etree.ElementTree는 XXE에 안전한가요?

CWE-611: XML 외부 엔티티 참조참고 페이지
짧은 답변

파일 읽기나 네트워크를 이용하는 전형적인 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:
문서 유형 선언이 엔티티를 정의하고 설정에 따라 외부 리소스를 요청할 수 있습니다.
1단계

신뢰할 수 없는 XML이 파서에 도달

업로드, SVG, SAML 메시지, 연동 데이터가 신뢰 경계를 넘어 XML API에 전달됩니다.

2단계

구현과 옵션이 동작을 결정

표준 라이브러리는 Expat, lxml은 libxml2를 사용합니다. 버전, 외부 확인, 확장, 플래그가 가능한 동작을 정합니다.

3단계

확인 또는 확장이 영향을 줌

외부 확인이 켜져 있으면 엔티티가 로컬·원격 리소스를 요청할 수 있습니다. 취약한 확장이나 긴 토큰도 자원을 소진시킬 수 있습니다.

4단계

프로세스 권한이 영향을 제한

파일 권한과 네트워크 egress가 XXE 영향을 제한합니다. 입력 제한, 파서 업데이트, 안전 설정, 격리로 노출을 줄이세요.

현재 Python 보안 지침에 따르면 Expat은 기본적으로 로컬 파일이나 네트워크에 접근하지 않습니다. 다만 2.7.2 미만 버전은 billion laughs, quadratic blowup, 긴 토큰, 과도한 메모리 사용에 취약할 수 있습니다. Python이 번들 또는 시스템 Expat을 사용할 수 있으므로 pyexpat.EXPAT_VERSION을 확인하세요. 이는 버전별 리소스 고갈 위험이며, 현재 ElementTree가 외부 엔티티를 확인한다는 뜻은 아닙니다.

강화 — DTD와 외부 리소스 거부
# 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)
구분해야 할 세 가지 XML 파서 위험
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를 실행하지 마세요. 파싱과 압축 해제 전에 크기를 제한하세요.

파서 기본값을 버전별로 검토해야 하는 이유

“Python XML”은 단일 설정이 아닙니다. 표준 라이브러리는 빌드에 따라 번들이나 시스템 Expat을 사용합니다. lxml은 libxml2를 감싸며 5.0에서 엔티티 기본값을 바꿨고 wrapper와 명시적 설정도 동작을 바꿀 수 있습니다. Python, Expat, lxml, libxml2 버전을 기록하고 신뢰할 수 없는 입력을 받는 각 지점에 옵션을 명시하세요.

자주 묻는 질문

ElementTree가 기본 설정으로 외부 엔티티에서 파일을 읽나요?

아니요. Python 문서에 따르면 Expat은 기본적으로 로컬 파일이나 네트워크를 이용하지 않습니다. 버전을 확인하고 XXE 파일 접근과 오래된 파서의 자원 고갈을 구별하세요.

현재 lxml은 기본 설정에서 위험한가요?

버전을 구분해야 합니다. 현재 XMLParser는 내부 엔티티만 확인하고 DTD와 네트워크를 기본 사용하지 않습니다. lxml 5.0에서 resolve_entities 기본값이 True에서 internal로 바뀌었습니다. 옵션과 wrapper를 확인하세요.

defusedxml이 모든 DTD를 기본 거부하나요?

아니요. 엔티티와 외부 참조는 차단하지만 forbid_dtd는 False입니다. DTD가 불필요하면 True로 설정하세요.

안전한 파서가 모든 XML 자원 위험을 없애나요?

아니요. 압축 및 해제 후 크기를 제한하고 파서를 업데이트하며 huge_tree를 피하세요. 제한 없는 loader로 신뢰할 수 없는 XInclude를 처리하지 마세요.

주요 참고 자료

함께 보기 XML 외부 엔터티(XXE) 인젝션(CWE-611): 동작 원리와 방어 방법 — flawopen.com .NET XmlReader는 XXE에 안전한가요? Java XXE: DocumentBuilderFactory는 기본적으로 안전한가요?