XPathインジェクション

Java XPathインジェクションと変数バインド

説明

XPath式には、XMLノードを選ぶパスだけでなく、条件式、関数、演算子も含まれます。リクエスト文字列を compile、evaluate、evaluateExpression、dom4jの選択メソッド、Jaxenの式コンストラクターの式を指定する引数へ連結したり、そのまま渡したりすると、攻撃者がデータ値ではなくクエリ構造を選択できるおそれがあります。

式とデータを区別してください。xpath.evaluate(expression, document) の第1引数は式、第2引数は検索対象のコンテキストです。外部の値は固定の式の変数として宣言し、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 が条件式の構造を変える可能性があります。

java
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のトークンを含む値も、式の構文ではなく変数の値として扱われます。

java
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では、XPathの作成時にJaxenの VariableContext を渡せます。

java
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変数はデータ値を表し、要素名や軸などの構文は置き換えられません。そのような構造を選択する場合は、入力を検証して連結するのではなく、サーバーが管理する完全な式から選んでください。

java
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機能とリソース制限

管理者向け診断ツールなどで式全体の入力が必要な場合、一般的な無害化で任意の式を安全にすることはできません。機能を分離して厳格に認可し、必要な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はそのアルゴリズムの括弧式の評価を修正しています。依存ライブラリの更新は重要ですが、攻撃者が選ぶ式の意味を制限するものではありません。

参考資料