XSLT 변환 취약점 (XSLT Injection)

공격자가 제어하는 XSLT 스타일시트 컴파일

설명

XSLT 스타일시트는 XML 형식이지만 일반 XML 데이터가 아니라 변환 명령을 담은 실행 가능한 템플릿 언어 콘텐츠입니다. 요청 본문, 매개변수, 업로드 파일 또는 제한되지 않은 경로·식별자가 스타일시트 소스를 결정한 뒤 애플리케이션이 이를 컴파일하면 공격자가 변환 로직을 제어할 수 있습니다.

Java에서는 다음과 같은 호출이 스타일시트를 컴파일하거나 변환 명령으로 사용합니다.

  • JAXP TransformerFactory.newTransformer(Source)와 newTemplates(Source)
  • SAXTransformerFactory.newTransformerHandler(Source)와 newXMLFilter(Source)
  • Saxon XsltCompiler.compile(Source)와 compilePackage(Source)

반면 TransformerFactory.newTransformer()는 스타일시트가 없는 항등 변환을 만들며, 이미 신뢰할 수 있는 스타일시트로 생성한 Transformer.transform(Source, Result)의 첫 번째 인자는 변환할 XML 데이터입니다. XML 데이터의 안전한 파싱은 별도의 요구 사항이며, 스타일시트에 대한 신뢰와 구분해야 합니다.

잠재적 영향

  • 변환 로직 및 출력 조작: 공격자가 템플릿, 조건식, XPath 식과 출력 구조를 바꿀 수 있습니다.
  • 파일 또는 네트워크 접근: 외부 접근이 허용된 프로세서는 외부 DTD, xsl:import, xsl:include, document() URI를 통해 로컬 파일이나 네트워크 리소스에 접근할 수 있습니다.
  • 자원 고갈: 복잡하거나 반복적인 스타일시트가 CPU와 메모리를 과도하게 소비할 수 있습니다.
  • 코드 호출: 확장 함수나 이에 준하는 프로세서별 기능이 활성화된 환경에서는 애플리케이션 또는 서드파티 코드를 호출할 수 있습니다. JDK 25의 jdk.xml.enableExtensionFunctions 기본값은 false이므로 코드 실행 가능성을 모든 환경에 일반화해서는 안 됩니다.

해결 방법

  1. 스타일시트를 서버 소유로 유지합니다.
    • 신뢰할 수 없는 사용자가 제공하거나 수정한 전체 스타일시트를 컴파일하지 않습니다.
    • 사용자가 출력 형식을 선택해야 한다면 허용된 작은 선택 집합을 애플리케이션이 소유하는 완전하고 변경 불가능한 스타일시트 리소스에 매핑합니다. 요청 문자열을 파일명이나 리소스 경로에 직접 이어 붙이지 않습니다.
    • XSLT 전체 문서에 적용할 수 있는 범용 이스케이프나 안전한 sanitize 헬퍼는 없습니다.
  2. JAXP를 심층 방어로 명시적으로 강화합니다.
    • XMLConstants.FEATURE_SECURE_PROCESSING을 true로 설정합니다.
    • XMLConstants.ACCESS_EXTERNAL_DTD와 XMLConstants.ACCESS_EXTERNAL_STYLESHEET을 빈 문자열로 설정해 모든 외부 프로토콜을 차단합니다.
    • jdk.xml.enableExtensionFunctions를 활성화하지 않습니다.
    • URIResolver가 null이 아닌 Source를 반환하면 해당 해석에는 외부 접근 속성이 적용되지 않습니다. 리졸버는 접근을 거부하거나 승인된 로컬 리소스만 반환해야 합니다.
  3. 현재 런타임과 제한 설정을 사용합니다.
    • Java 25의 <java_home>/conf/jaxp-strict.properties.template을 별도 설정 파일로 복사하고 -Djava.xml.config.file=...로 테스트하면 더 엄격한 JAXP 설정에 대한 애플리케이션 호환성을 확인할 수 있습니다.
    • 애플리케이션에 맞는 처리량, 깊이, XPath 연산자, 시간 및 메모리 한도를 적용합니다. 이러한 제한은 피해를 줄일 뿐 공격자가 제어하는 스타일시트를 안전하게 만들지는 않습니다.
  4. 스타일시트 작성 기능이 꼭 필요하면 격리합니다.
    • 인가된 사용자만 접근하게 하고, 최소한의 XML 데이터와 권한만 가진 별도 서비스에서 실행하며, 외부 리소스와 확장 함수를 비활성화합니다.

예시

변경 전

java
import java.io.StringReader;
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

public final class InsecureXslt {
    public void transform(HttpServletRequest request) throws Exception {
        Source stylesheet = new StreamSource(request.getInputStream());
        Transformer transformer = TransformerFactory.newInstance().newTransformer(stylesheet);

        Source xml = new StreamSource(new StringReader("<report/>"));
        transformer.transform(xml, new StreamResult(System.out));
    }
}

요청 본문 전체가 변환 명령으로 컴파일되므로 공격자가 스타일시트를 직접 제어합니다.

변경 후

java
import java.io.InputStream;
import java.io.StringReader;
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

public final class SecureXslt {
    private static final String REPORT_STYLESHEET = "/xsl/report.xsl";

    public void render(HttpServletRequest request) throws Exception {
        TransformerFactory factory = TransformerFactory.newInstance();
        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");

        try (InputStream stream = SecureXslt.class.getResourceAsStream(REPORT_STYLESHEET)) {
            if (stream == null) {
                throw new IllegalStateException("Missing trusted stylesheet");
            }
            Transformer transformer = factory.newTransformer(new StreamSource(stream));
            Source xml = new StreamSource(new StringReader(request.getParameter("xml")));
            transformer.transform(xml, new StreamResult(System.out));
        }
    }
}

스타일시트는 고정된 애플리케이션 리소스이며 요청 데이터는 변환할 XML로만 사용됩니다. 외부 DTD와 스타일시트 접근을 명시적으로 차단하는 설정은 신뢰 경계를 대신하지 않는 심층 방어입니다.

참조