설명
XPath 표현식은 XML 노드를 선택하는 위치 경로뿐 아니라 조건식, 함수, 연산자를 포함할 수 있습니다. 요청에서 받은 문자열을 compile, evaluate, evaluateExpression, dom4j 선택 메서드 또는 Jaxen 표현식 생성자의 표현식 인자에 연결하거나 그대로 전달하면 공격자가 데이터 값이 아니라 쿼리 구조를 선택할 수 있습니다.
표현식과 데이터는 구분해야 합니다. xpath.evaluate(expression, document)에서 첫 번째 인자는 해석할 표현식이고 두 번째 인자는 조회할 컨텍스트입니다. 외부 값은 고정 표현식 안의 변수로 선언한 뒤 XPathVariableResolver 또는 dom4j/Jaxen VariableContext를 통해 전달해야 합니다.
XPath 1.0 문자열 리터럴에는 모든 입력에 적용할 수 있는 범용 이스케이프 구문이 없습니다. 따옴표 제거, 일반적인 escape·sanitize 헬퍼, 형식 정규식 또는 사전 컴파일만으로 데이터와 표현식 구조가 분리되지는 않습니다.
잠재적 영향
- 허용되지 않은 XML 데이터 조회: 공격자가 위치 경로나 조건식을 변경해 원래 필터 밖의 노드를 선택할 수 있습니다.
- 보안 로직 우회: 애플리케이션이 XPath 결과를 인증, 권한 또는 정책 판단에 사용하면 해당 판단을 조작할 수 있습니다.
- 자원 소모: 애플리케이션이 전체 표현식을 허용할 때 복잡한 경로와 연산이 비싼 평가를 유발할 수 있습니다. 단순히 XPath를 사용하는 모든 코드가 서비스 거부로 이어지는 것은 아닙니다.
해결 방법
가장 중요한 원칙은 XPath 표현식은 서버에서 정의하고, 외부 입력은 데이터 값으로만 바인딩하는 것입니다.
- Java XPath에서는 고정 표현식에
$name변수를 사용하고XPath#setXPathVariableResolver로 값을 제공합니다. 리졸버는 표현식을 컴파일하거나 평가하기 전에 설정합니다. - dom4j와 Jaxen에서는 고정 표현식과
VariableContext를 함께 사용합니다. - 요소 이름, 축 또는 조건식 조각처럼 변수로 나타낼 수 없는 구조를 사용자가 선택해야 한다면, 외부 키를 작은 유한 집합의 완전한 서버 정의 표현식에 매핑합니다. 허용된 입력 문자열 자체를 표현식에 이어 붙이지 않습니다.
- 입력 검증은 길이, 식별자 형식과 같은 비즈니스 제약에 사용할 수 있지만 변수 바인딩의 대체 수단으로 취급하지 않습니다.
- 신뢰할 수 없는 표현식을 먼저 만든 뒤
compile하는 것은 안전 조치가 아닙니다. 컴파일 단계 자체가 해당 문자열을 XPath 구문으로 해석합니다.
예시
Java XPath
변경 전
다음 코드는 요청값을 따옴표 안에 이어 붙이므로 profileId가 조건식의 구조를 바꿀 수 있습니다.
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
final class VulnerableXPathLookup {
static Node findProfile(HttpServletRequest request, Document document)
throws XPathExpressionException {
String profileId = request.getParameter("profileId");
String expression = "/directory/profile[@id='" + profileId + "']";
XPath xpath = XPathFactory.newInstance().newXPath();
return xpath.evaluateExpression(expression, document, Node.class);
}
}
변경 후
표현식은 상수로 유지하고 요청값을 변수로 바인딩합니다. 따옴표나 XPath 토큰이 포함된 값도 표현식 구문이 아니라 변수 값으로 처리됩니다.
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
final class SafeXPathLookup {
static Node findProfile(HttpServletRequest request, Document document)
throws XPathExpressionException {
String profileId = request.getParameter("profileId");
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setXPathVariableResolver(variable -> switch (variable.getLocalPart()) {
case "profileId" -> profileId;
default -> null;
});
String expression = "/directory/profile[@id=$profileId]";
return xpath.evaluateExpression(expression, document, Node.class);
}
}
XPath 객체는 스레드 안전하지 않고 재진입도 지원하지 않습니다. 요청별 값을 캡처한 리졸버를 공유 XPath 객체에 계속 바꾸어 설정하지 말고, 예시처럼 요청 범위에서 사용하거나 애플리케이션이 동시 접근을 안전하게 관리해야 합니다.
dom4j/Jaxen 변수 바인딩
dom4j는 Jaxen의 VariableContext를 XPath 생성 시 함께 받을 수 있습니다.
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Node;
import org.dom4j.XPath;
import org.jaxen.SimpleVariableContext;
final class SafeDom4jXPathLookup {
static Node findProfile(Document document, String profileId) {
SimpleVariableContext variables = new SimpleVariableContext();
variables.setVariableValue("profileId", profileId);
XPath xpath = DocumentHelper.createXPath(
"/directory/profile[@id=$profileId]", variables);
return xpath.selectSingleNode(document);
}
}
서버가 정의한 표현식 선택
XPath 변수는 데이터 값에 적합하지만 요소 이름이나 축 같은 구문 구조를 대신할 수는 없습니다. 이 경우 사용자 입력을 검증한 뒤 그대로 연결하지 말고, 서버가 소유한 전체 표현식 중 하나를 선택합니다.
final class ServerOwnedXPathView {
static String expressionFor(String requestedView) {
return switch (requestedView) {
case "active" -> "/directory/profile[@active='true']";
case "locked" -> "/directory/profile[@locked='true']";
default -> "/directory/profile";
};
}
}
엔진에 전달되는 값은 항상 코드에 정의된 세 표현식 중 하나이며 requestedView 자체는 XPath 문자열에 포함되지 않습니다.
임의 XPath 기능과 자원 제한
관리자용 진단 도구처럼 전체 XPath 입력이 제품 요구사항인 경우에는 일반적인 무해화로 의미 있는 임의 표현식을 안전하게 바꿀 수 없습니다. 기능을 별도로 격리하고 강하게 인가하며, 필요한 XML 데이터만 제공하고 요청 빈도와 결과 크기를 제한해야 합니다.
Java SE 25의 jdk.xml.xpathExprGrpLimit와 jdk.xml.xpathExprOpLimit는 XPath 그룹과 연산자 수를 제한합니다. 애플리케이션 요구 범위에 맞게 유지하되, 이 제한은 자원 소모의 영향을 줄이는 심층 방어일 뿐 표현식 인젝션을 제거하지 않습니다.
2026년 9월 1일에 확인한 안정 릴리스는 dom4j 2.2.0과 Jaxen 2.0.6입니다. Jaxen 2.0.5는 재귀적인 코어 파서 경로를 반복 알고리즘으로 바꾸고 남은 스택 오버플로를 JaxenException으로 처리했으며, 2.0.6은 새 반복 알고리즘의 괄호식 평가 오류를 수정했습니다. 의존성 업데이트는 중요하지만 공격자가 선택한 표현식의 의미를 제한하지는 않습니다.
참조
- Java SE 25
XPathAPI - Java SE 25
java.xml모듈과 JAXP 처리 제한 - Oracle Java SE 25 Security Developer's Guide
- dom4j 2.2.0 릴리스
- dom4j 2.2.0
XPath인터페이스 - Jaxen 2.0.6 릴리스
- Jaxen 공식 릴리스 이력
- CWE-643: Improper Neutralization of Data within XPath Expressions
- OWASP Top 10:2025 A05 Injection
- OWASP Top 10:2021 A03 Injection
- OWASP ASVS 5.0.0 V1.2.4