설명
XPath 인젝션은 신뢰할 수 없는 문자열이 XPath 표현식의 데이터가 아니라 쿼리 구문으로 해석될 때 발생합니다. 공격자는 따옴표, 논리 연산자, 경로 연산자 또는 함수를 삽입해 조건식을 항상 참으로 만들거나 애플리케이션이 의도하지 않은 노드를 선택할 수 있습니다.
XPathExpression.Compile은 표현식을 미리 구문 분석할 뿐 신뢰 경계를 만들지 않습니다. 사용자 입력으로 만든 표현식을 컴파일한 뒤 재사용해도 취약합니다. XmlNamespaceManager 역시 접두사와 네임스페이스를 해석할 뿐 쿼리 변수를 바인딩하지 않습니다.
이 취약점은 XML 마크업 구조를 바꾸는 XML 인젝션, 외부 엔터티로 파일이나 네트워크 자원을 읽는 XXE와 서로 다릅니다.
잠재적 영향
- 인증 또는 권한 조건을 우회할 수 있습니다.
- 접근할 수 없어야 하는 XML 노드와 속성을 조회할 수 있습니다.
- 오류 응답이나 반복 질의를 이용해 XML 구조와 데이터를 추론할 수 있습니다.
- 공격자가 복잡한 표현식을 제어하면 CPU 사용량이나 응답 시간이 증가할 수 있습니다.
해결 방법
XPath 구조는 개발자가 제어하고, 신뢰할 수 없는 값은 구문과 분리하세요.
- 임의 문자열 값은 고정된 표현식의 변수로 바인딩합니다. .NET에서는 사용자 정의
XsltContext와IXsltContextVariable을 구현하고XPathExpression.SetContext로 지정할 수 있습니다. - 숫자처럼 도메인이 명확한 값은 엄격한 형식과 범위로 파싱한 뒤 해당 타입의 정규화된 값을 올바른 XPath 위치에 사용합니다. 문자열 위치에 숫자 검증을 재사용하면 안 됩니다.
- 사용자가 여러 조회 중 하나를 선택해야 한다면 입력 토큰을 개발자가 작성한 고정 XPath에 매핑합니다. 입력 자체를 표현식으로 전달하지 않습니다.
- 완성된 표현식을 직접 허용해야 한다면 변경되지 않는 로컬 리터럴 허용 목록과 정확히 일치하는 값만 사용합니다. 허용 목록의 내용과 변경 권한은 애플리케이션이 통제해야 합니다.
XML/HTML 인코더, SecurityElement.Escape, 따옴표 치환, XmlNamespaceManager 또는 이름이 Sanitize인 임의 도우미는 일반적인 XPath parameterization이 아닙니다. XPath 문자열 리터럴, 숫자, 이름, 경로 등 삽입 위치마다 문법이 다르므로 임의 표현식 조립을 피하는 것이 우선입니다.
예시
변경 전
using Microsoft.AspNetCore.Mvc;
using System.Xml;
XmlNode? FindUser(XmlDocument document, [FromQuery] string name)
{
var expression = "//users/user[@name='" + name + "']";
return document.SelectSingleNode(expression);
}
name에 ' or '1'='1 같은 값이 들어오면 조건식의 구조가 달라집니다.
변경 후
문자열 값은 변수로 바인딩
다음 예시는 고정된 $name 변수를 사용자 정의 XsltContext로 해석합니다. 입력값은 XPathExpression.Compile에 전달되지 않습니다.
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
sealed class VariableContext : XsltContext
{
private readonly IReadOnlyDictionary<string, string> values;
public VariableContext(
NameTable nameTable,
IReadOnlyDictionary<string, string> values) : base(nameTable)
{
this.values = values;
}
public override bool Whitespace => true;
public override int CompareDocument(string baseUri, string nextBaseUri) =>
StringComparer.Ordinal.Compare(baseUri, nextBaseUri);
public override bool PreserveWhitespace(XPathNavigator node) => true;
public override IXsltContextFunction ResolveFunction(
string prefix,
string name,
XPathResultType[] argumentTypes) =>
throw new XPathException($"Unknown function: {prefix}:{name}");
public override IXsltContextVariable ResolveVariable(
string prefix,
string name)
{
if (prefix.Length == 0 && values.TryGetValue(name, out var value))
{
return new BoundString(value);
}
throw new XPathException($"Unknown variable: {prefix}:{name}");
}
}
sealed class BoundString : IXsltContextVariable
{
private readonly string value;
public BoundString(string value) => this.value = value;
public bool IsLocal => false;
public bool IsParam => true;
public XPathResultType VariableType => XPathResultType.String;
public object Evaluate(XsltContext context) => value;
}
sealed class Search
{
public XPathNavigator? FindUser(XPathNavigator navigator, [FromQuery] string name)
{
var expression = XPathExpression.Compile("//users/user[@name=$name]");
var values = new Dictionary<string, string> { ["name"] = name };
expression.SetContext(new VariableContext(new NameTable(), values));
return navigator.SelectSingleNode(expression);
}
}
실제 구현에서는 필요한 변수 이름과 타입만 허용하고, 알 수 없는 변수와 함수를 실패 처리하세요. 네임스페이스 접두사가 필요하면 같은 context에 고정된 매핑을 추가합니다.
숫자 도메인은 엄격히 파싱
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Xml;
XmlNode? FindUser(XmlDocument document, [FromQuery] string id)
{
if (!int.TryParse(
id,
NumberStyles.None,
CultureInfo.InvariantCulture,
out var numericId))
{
return null;
}
return document.SelectSingleNode($"//users/user[@id={numericId}]");
}
이 예시는 음수 기호나 공백 없이 10진 정수만 받고, 정규화된 int를 따옴표 없는 숫자 위치에 사용합니다. 업무상 허용 범위도 별도로 검사해야 합니다.
선택 토큰을 고정 XPath에 매핑
using Microsoft.AspNetCore.Mvc;
using System.Xml;
XmlNode? FindView(XmlDocument document, [FromQuery] string view)
{
string? expression = view switch
{
"active" => "//users/user[@active='true']",
"admin" => "//users/user[@role='admin']",
_ => null
};
return expression is null
? null
: document.SelectSingleNode(expression);
}
XPath 구문을 보호해도 조회 결과의 인증·권한 검사, XXE 방지와 XML 구조 보호는 별도로 적용해야 합니다.
관련 기준
- CWE: CWE-643: Improper Neutralization of Data within XPath Expressions, CWE 4.20
- OWASP Top 10: A05:2025 Injection, A03:2021 Injection
- OWASP ASVS: v5.0.0-1.2.7
참조
- .NET 및 .NET Core 공식 지원 정책
- Microsoft Learn: CA3008 - XPath 인젝션 취약점 검토
- Microsoft Learn: XmlNode.SelectNodes
- Microsoft Learn: XPathNavigator
- Microsoft Learn: XPathExpression.Compile
- Microsoft Learn: XPathExpression.SetContext
- Microsoft Learn: 사용자 정의 XPath 함수와 변수
- Microsoft Learn: LINQ to XML XPath Extensions
- CodeQL: C# XPath injection
- 소프트웨어 보안약점 진단가이드 2021
- 주요정보통신기반시설 취약점 분석·평가 기준