XPath 인젝션

C# XPath 인젝션

설명

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 문자열 리터럴, 숫자, 이름, 경로 등 삽입 위치마다 문법이 다르므로 임의 표현식 조립을 피하는 것이 우선입니다.

예시

변경 전

csharp
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에 전달되지 않습니다.

csharp
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에 고정된 매핑을 추가합니다.

숫자 도메인은 엄격히 파싱

csharp
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에 매핑

csharp
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 구조 보호는 별도로 적용해야 합니다.

관련 기준

참조