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