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のパラメーター化ではありません。文字列リテラル、数値、名前、パスでは文法が異なるため、任意の式を組み立てないことを優先してください。

例

変更前

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);
    }
}

必要な変数名と型だけを許可し、未知の変数や関数は拒否してください。名前空間の接頭辞が必要なら、同じコンテキストに固定の対応付けを追加します。

数値の範囲を厳密に解析

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構造の保護は別途必要です。

関連基準

参考資料