XPath 인젝션 취약점이 발생하는 원인과 XML 데이터 조회문이 조작되는 과정, 취약한 Java 코드 및 변수 바인딩을 이용한 대응 방법을 알아봅니다.
웹 애플리케이션은 데이터베이스뿐 아니라 XML 문서에 저장된 정보를 검색하기도 한다. 사용자 계정, 상품 정보, 환경설정처럼 계층적인 데이터를 XML로 관리하고 XPath를 이용해 필요한 값을 찾는 방식이다.
XPath 조회문을 만들 때 사용자가 입력한 값을 문자열로 직접 연결하면 공격자가 조회 조건의 구조를 변경할 수 있다. 이로 인해 인증 조건이 무력화되거나 원래 공개해서는 안 되는 XML 데이터가 노출될 수 있다. 이러한 보안 문제를 XPath 인젝션 취약점이라고 한다.
XPath란?
XPath는 XML 문서 안에서 특정 요소나 속성을 선택하기 위한 조회 언어다. 파일 시스템의 경로처럼 XML 요소의 위치를 지정할 수 있으며, 조건식을 사용해 원하는 데이터만 찾을 수도 있다.
다음과 같은 XML 사용자 정보가 있다고 가정해 보자.
<users>
<user>
<username>user01</username>
<passwordHash>...</passwordHash>
<role>user</role>
</user>
<user>
<username>admin</username>
<passwordHash>...</passwordHash>
<role>admin</role>
</user>
</users>
애플리케이션은 XPath를 이용해 username이 입력값과 일치하는 user 요소를 찾을 수 있다. 조회문이 개발자가 작성한 고정된 구조로 동작하고 입력값이 데이터로만 처리된다면 문제가 없다.
하지만 사용자 입력이 XPath 문자열 안에 그대로 들어가면 따옴표, 괄호와 논리 연산자 등이 조회문 문법으로 해석될 수 있다.
XPath 인젝션 취약점이란?
XPath 인젝션은 외부 입력값이 XPath 조회문의 조건과 구조에 영향을 주는 취약점이다. 공격자는 정상적인 검색값 대신 XPath 문법으로 해석될 수 있는 값을 전달해 원래 조건을 변경하려고 시도한다.
SQL 인젝션이 SQL 쿼리를 조작해 데이터베이스 조회 조건을 바꾸는 공격이라면 XPath 인젝션은 XML 문서의 조회 조건을 조작한다는 차이가 있다.
XPath는 SQL처럼 데이터베이스 제품마다 크게 다른 문법을 사용하지 않는다. 애플리케이션이 XML 구조를 알고 있고 오류 메시지까지 자세하게 보여준다면 공격자는 XML 요소와 속성 이름을 추측하려고 할 수 있다.
XPath 인젝션의 핵심은 외부 입력값이 XML 데이터가 아니라 XPath 조회문의 일부로 해석된다는 점이다.
XPath 인젝션이 발생하는 원인
가장 흔한 원인은 로그인 화면에서 받은 아이디와 비밀번호를 XPath 문자열에 직접 연결하는 것이다. 개발자는 두 값이 모두 일치하는 사용자만 검색하려고 하지만, 공격자가 입력값에 XPath 연산자를 포함하면 조건의 의미가 달라질 수 있다.
검색 기능에서도 같은 문제가 발생한다. 상품명, 사용자 이름이나 부서명을 입력받아 XPath를 동적으로 만들면서 입력값을 안전하게 처리하지 않으면 여러 노드가 한꺼번에 선택될 수 있다.
특정 특수문자만 삭제하는 블랙리스트 방식도 충분하지 않다. XPath 표현식은 함수, 논리 연산, 문자열 비교 등 다양한 문법을 지원하므로 차단할 문자를 계속 추가하는 방식으로 모든 우회를 막기 어렵다.
XPath API에는 SQL의 PreparedStatement와 완전히 같은 표준 기능이 있는 것은 아니다. 그렇기 때문에 조회문을 문자열로 조립하지 않고 고정된 XPath 표현식과 변수 바인딩을 사용하거나, XML을 객체로 변환한 뒤 일반 코드로 값을 비교하는 방식이 필요하다.
XPath 인젝션에 취약한 Java 코드
다음은 사용자가 입력한 아이디와 비밀번호를 XPath 조회문에 직접 연결하는 코드다.
public boolean login(
String username,
String password,
Document document) throws Exception {
XPath xpath =
XPathFactory.newInstance()
.newXPath();
String expression =
"/users/user[" +
"username/text()='" + username + "'" +
" and " +
"password/text()='" + password + "'" +
"]";
Node user = (Node) xpath.evaluate(
expression,
document,
XPathConstants.NODE
);
return user != null;
}
코드는 아이디와 비밀번호가 모두 일치하는 user 요소를 찾으려고 한다. 하지만 username과 password가 따옴표 안에 문자열로 직접 들어간다.
공격자가 XPath 문법에 영향을 줄 수 있는 값을 입력하면 문자열의 경계가 달라지거나 새로운 조건이 추가될 수 있다. 그 결과 정상적인 비밀번호를 확인하지 않았는데도 특정 사용자 노드가 선택될 가능성이 생긴다.
예시에서는 이해를 돕기 위해 비밀번호를 XML에서 직접 비교하지만 실제 서비스에서는 비밀번호 원문을 XML이나 데이터베이스에 저장해서는 안 된다. 안전한 단방향 해시로 보관하고 애플리케이션에서 해시 검증을 수행해야 한다.
XML 데이터 조회문이 조작되는 원리
정상적인 요청에서는 사용자가 입력한 값이 XPath 조건의 비교 대상으로 사용된다. 서버는 XML 문서에서 해당 값과 정확히 일치하는 노드만 반환한다.
취약한 코드에서는 입력값과 XPath 문법이 하나의 문자열로 합쳐진다. 공격자는 문자열의 경계를 변경하거나 논리 조건을 추가해 원래 조회문과 다른 결과가 나오도록 시도할 수 있다.
애플리케이션이 검색 결과가 존재하는지만 보고 로그인 성공을 결정한다면 비밀번호 조건이 무력화됐을 때 인증 우회로 이어질 수 있다. 검색 기능에서는 여러 사용자나 관리용 데이터가 한꺼번에 반환될 가능성이 있다.
화면에 결과가 직접 표시되지 않더라도 응답 내용과 오류 메시지의 차이를 이용해 XML 데이터의 존재 여부를 추측할 수 있다. 조건이 참일 때와 거짓일 때 서버 응답이 다르면 공격자가 이를 반복해 내부 구조를 알아내려고 할 수 있다.
XPath 인젝션으로 발생할 수 있는 피해
로그인 기능에서 XPath 인젝션이 발생하면 일반 사용자가 다른 계정이나 관리자 계정으로 인증을 우회할 수 있다. 이후 해당 계정의 권한으로 중요 기능에 접근할 가능성이 생긴다.
검색 기능에서는 사용자 이름, 이메일, 부서와 권한 정보 등 XML에 저장된 데이터가 노출될 수 있다. 애플리케이션이 조회 결과를 화면이나 API 응답에 그대로 포함한다면 피해 범위가 더 커질 수 있다.
XPath 오류 메시지가 외부에 자세히 표시되면 공격자는 조회문의 구조와 XML 요소 이름을 파악하는 데 이용할 수 있다. 따라서 오류 내용은 서버 로그에만 기록하고 사용자에게는 일반적인 실패 메시지를 제공해야 한다.
XPath 인젝션 자체는 일반적으로 XML 조회 조건을 조작하는 문제다. 하지만 노출된 계정 정보나 인증 권한이 다른 공격에 이용되면 전체 시스템 침해로 피해가 확대될 수 있다.
XPath 변수 바인딩을 이용한 안전한 Java 코드
XPath 조회문에 사용자 입력을 문자열로 연결하지 않고 변수로 전달하면 입력값과 조회문 구조를 분리할 수 있다.
public boolean login(
String username,
String password,
Document document) throws Exception {
if (username == null ||
!username.matches(
"^[A-Za-z0-9._-]{3,50}$")) {
return false;
}
if (password == null ||
password.isBlank()) {
return false;
}
Map<QName, Object> variables =
new HashMap<>();
variables.put(
new QName("username"),
username
);
XPath xpath =
XPathFactory.newInstance()
.newXPath();
xpath.setXPathVariableResolver(
name -> variables.get(name)
);
XPathExpression userExpression =
xpath.compile(
"/users/user[username=$username]"
);
Node user = (Node)
userExpression.evaluate(
document,
XPathConstants.NODE
);
if (user == null) {
return false;
}
XPathExpression hashExpression =
xpath.compile(
"passwordHash/text()"
);
String storedHash = (String)
hashExpression.evaluate(
user,
XPathConstants.STRING
);
return passwordEncoder.matches(
password,
storedHash
);
}
이 코드는 사용자 이름을 XPath 문자열에 직접 넣지 않는다. XPath 표현식은 /users/user[username=$username]으로 고정돼 있고 실제 사용자 이름은 변수 해석기를 통해 데이터로 전달된다.
공격자가 XPath 문법에 사용되는 문자를 입력하더라도 조회문의 일부로 결합되지 않는다. 사용자 이름을 이용해 계정 노드를 찾은 뒤 비밀번호는 애플리케이션의 passwordEncoder.matches()로 안전하게 비교한다.
입력값 검증도 함께 적용한다. 사용자 ID에 허용할 문자와 길이가 정해져 있다면 허용 목록을 기준으로 검사하는 것이 좋다. 다만 입력값 검증만으로 XPath 인젝션 대응을 끝내지 말고 조회문과 입력 데이터를 구조적으로 분리해야 한다.
고정된 XPath 표현식을 사용해야 한다
검색 조건의 종류가 여러 개라면 사용자에게 XPath 문장을 직접 입력하게 해서는 안 된다. 서버에서 허용된 검색 조건을 미리 정의하고 사용자는 검색 유형과 값만 전달하도록 구성해야 한다.
예를 들어 이름 검색과 부서 검색을 제공한다면 name, department 같은 식별자를 서버의 고정된 XPath 표현식과 연결한다. 사용자가 XPath 함수나 경로를 직접 결정할 수 없도록 하는 것이 중요하다.
조회할 XML 경로가 고정돼 있다면 XPath를 사용하지 않고 DOM이나 XML 객체 매핑 기능으로 필요한 요소를 찾은 뒤 Java 코드에서 값을 비교하는 방법도 있다. 동적으로 표현식을 만들 필요가 없다면 이 방식이 더 단순할 수 있다.
비밀번호와 권한은 다시 검증해야 한다
사용자 계정을 찾았다는 이유만으로 인증을 완료해서는 안 된다. 비밀번호는 안전한 해시 알고리즘을 이용해 저장하고 애플리케이션에서 검증해야 한다.
XML에 저장된 role 값이 있다고 해도 요청에서 전달된 역할이나 조회 결과를 바로 권한으로 사용하지 말아야 한다. 로그인에 성공한 계정의 식별자를 기준으로 서버가 권한 정보를 다시 확인해야 한다.
조회 결과가 여러 건이라면 첫 번째 사용자만 임의로 선택해서는 안 된다. 사용자 ID는 유일해야 하며, 예상보다 많은 노드가 반환되면 인증을 중단하고 오류로 처리하는 것이 안전하다.
XML 파서 보안 설정도 필요하다
XPath 인젝션과 XXE는 서로 다른 취약점이지만 동일한 XML 처리 과정에서 함께 발생할 수 있다.
XPath 인젝션은 XPath 조회문에 외부 입력값이 포함돼 조건이 조작되는 문제다. XXE는 XML 파서가 외부 개체를 처리하면서 서버의 파일이나 네트워크 자원을 참조하는 문제다.
외부에서 전달된 XML 문서를 XPath로 조회한다면 XPath 변수 바인딩뿐 아니라 XML 파서에서 DTD와 외부 개체 기능도 비활성화해야 한다. XPath 인젝션만 막고 XML 파서 설정을 그대로 두면 XXE 위험이 남을 수 있다.
XML 문서의 최대 크기, 요소 개수와 중첩 깊이도 제한해야 한다. 지나치게 복잡한 XML 문서는 서버의 CPU와 메모리를 과도하게 사용할 수 있기 때문이다.
오류 메시지와 접근 권한 관리
XPath 문법 오류나 XML 구조 정보가 사용자에게 그대로 표시되지 않도록 해야 한다. 외부 응답에는 일반적인 검색 실패 메시지를 제공하고 상세 오류는 서버 로그에만 기록한다.
애플리케이션은 XML 파일과 저장 경로에 필요한 최소 권한만 가져야 한다. 조회만 필요한 기능이라면 원본 XML 파일에 대한 쓰기 권한을 부여하지 않는 것이 좋다.
반복적인 XPath 오류, 특수문자가 포함된 검색 요청과 비정상적으로 많은 조회 결과를 기록하고 모니터링해야 한다. 로그인 기능에서는 계정과 IP를 기준으로 Rate Limiting을 적용해 자동화된 공격 시도를 제한할 수 있다.
XPath 인젝션 취약점 점검 항목
소스코드 점검에서는 XPath.evaluate(), XPath.compile()과 같은 함수를 먼저 찾는다. 해당 함수에 전달되는 표현식이 사용자 입력값과 문자열 결합으로 만들어지는지 확인해야 한다.
URL 파라미터, 요청 본문, 쿠키와 업로드 파일에서 받은 값이 XPath 문자열에 직접 포함되는지 추적한다. 사용자 입력값이 변수로 전달되는지, 아니면 조회문 문법의 일부가 되는지 구분해야 한다.
로그인 기능에서는 사용자 조회와 비밀번호 검증이 분리돼 있는지 확인한다. 비밀번호 원문을 XPath 조회 조건에 포함하지 않는지, 여러 노드가 반환됐을 때 인증이 중단되는지도 점검해야 한다.
외부 XML을 처리한다면 XXE 방어 설정, 문서 크기 제한과 XML 파일 접근 권한도 함께 확인한다. 오류 메시지에서 XPath와 XML 구조가 노출되지 않는지도 중요한 점검 항목이다.
자주 묻는 질문
XPath 인젝션과 SQL 인젝션은 같은 취약점인가요?
기본 원리는 비슷하지만 대상이 다르다. SQL 인젝션은 데이터베이스의 SQL 쿼리를 조작하고, XPath 인젝션은 XML 데이터를 검색하는 XPath 표현식을 조작한다.
특수문자만 제거하면 XPath 인젝션을 막을 수 있나요?
충분하지 않다. XPath는 다양한 연산자와 함수를 지원하므로 차단 목록 방식은 우회될 가능성이 있다. 조회문을 고정하고 입력값을 XPath 변수로 전달하는 것이 안전하다.
XPath에도 PreparedStatement가 있나요?
SQL의 PreparedStatement와 완전히 동일한 표준 기능은 없다. Java에서는 XPathVariableResolver를 이용해 입력값을 변수로 전달하거나, 고정된 XPath로 노드를 찾은 뒤 Java 코드에서 값을 비교할 수 있다.
XXE와 XPath 인젝션은 어떤 차이가 있나요?
XXE는 XML 외부 개체를 이용해 서버 파일이나 네트워크 자원에 접근하는 취약점이다. XPath 인젝션은 사용자 입력으로 XML 조회 조건을 변경하는 취약점이다. 외부 XML을 처리하는 기능에서는 두 문제를 모두 점검해야 한다.
마무리
XPath 인젝션은 외부 입력값이 XPath 조회문의 일부로 사용되면서 XML 검색 조건이 변경되는 취약점이다. 로그인 기능에서 발생하면 인증 우회로 이어질 수 있고, 검색 기능에서는 중요 데이터가 노출될 가능성이 있다.
안전한 대응을 위해서는 사용자 입력을 XPath 문자열에 직접 연결하지 않아야 한다. XPath 표현식은 서버에서 고정하고 입력값은 변수로 전달해야 한다. 사용자 ID와 검색값의 형식 및 길이도 허용 목록으로 제한하는 것이 좋다.
XPath 인젝션 방어의 핵심은 외부 입력값과 XML 조회문 구조를 분리하는 것이다. 변수 바인딩, 안전한 비밀번호 검증, XXE 차단과 최소 권한을 함께 적용해야 XML 데이터 조회문 조작을 효과적으로 예방할 수 있다.
핵심 키워드: XPath 인젝션, XPath Injection, XML 조회문 조작, XPath 인증 우회, XML 보안, XPath 변수 바인딩, XPath 취약점, XPath 인젝션 대응 방법