説明
XPathインジェクションは、外部から操作できる文字列をデータとして渡さず、パス式の構文に組み込むことで発生します。lxml は完全なXPath 1.0を扱うため、引用符、演算子、条件式を挿入されると、選択するノードが変わったり、本来の条件が無効になったりするおそれがあります。
Python標準ライブラリの xml.etree.ElementTree が扱うのは、完全なXPathではなく限定的なElementPath構文です。使える操作は少なくても、find()、findall()、findtext()、iterfind() の match を攻撃者が操作できれば、意図しないノードを選択される可能性があります。
想定される影響
- データの露出: 返されるノードの範囲を広げられると、他のユーザーやテナントの情報が漏れるおそれがあります。
- 認証・認可の回避: 検索結果をログインやアクセス制御に使っている場合、改変された条件でその判断を回避される可能性があります。
- 意図しない変更: 選択したノードを後から変更・削除するアプリケーションでは、誤った対象に変更が加わるおそれがあります。
- リソースの枯渇: 大きな文書に対して任意の複雑なXPathを許可すると、評価に多くのCPUを消費する場合があります。動的なElementPathが必ずサービス拒否につながるわけではありません。
対処方法
- XPathやElementPathの式を定数にし、文字列連結、f-string、
%、str.format()で構文を組み立てないでください。 lxmlでは、信頼できない値をXPath変数としてバインドしてください。例:tree.xpath("//user[@id=$uid]", uid=value)。変数で置き換えられるのは値であり、要素名、演算子、式の構造ではありません。- ユーザーがフィールド、パス、操作を選ぶ必要がある場合は、入力をサーバーが管理する完全な定数式に対応付けてください。トークンを直接使う場合は、正規化したうえで文字列全体を許可リストで検証してください。
- ElementTreeにはXPath変数をバインドするAPIがありません。
matchを定数にしてPythonで値を比較するか、サーバー側の対応表から定数式を選んでください。 - 数値の位置や値はPythonの組み込み数値型に変換し、業務上の範囲制約も確認してください。
- 関数名や汎用エスケープだけを安全性の根拠にしないでください。XPath文字列の処理は文脈に依存し、引用符の置換だけでは不十分になりがちです。変数のバインドか定数の対応表を優先してください。
- ユーザー作成の任意XPathを評価する必要がある場合は、通常の検索から分離し、専用の認可、入力サイズ、処理時間、リソースの制限を設けてください。
例
lxml
変更前
python
from flask import Flask, request
from lxml import etree
app = Flask(__name__)
XML_DATA = b"""
<accounts>
<account tenant="blue" id="1001"><email>blue@example.com</email></account>
<account tenant="red" id="2001"><email>red@example.com</email></account>
</accounts>
"""
@app.get("/account")
def account():
account_id = request.args.get("id", "")
root = etree.fromstring(XML_DATA)
# 危険: account_idがXPathの構文に直接組み込まれます。
expression = f"//account[@tenant='blue' and @id='{account_id}']"
matches = root.xpath(expression)
if not matches:
return ("not found", 404)
return etree.tostring(matches[0], encoding="unicode")
変更後
python
from flask import Flask, request
from lxml import etree
app = Flask(__name__)
XML_DATA = b"""
<accounts>
<account tenant="blue" id="1001"><email>blue@example.com</email></account>
<account tenant="red" id="2001"><email>red@example.com</email></account>
</accounts>
"""
@app.get("/account")
def account():
account_id = request.args.get("id", "")
root = etree.fromstring(XML_DATA)
# 式は定数にし、信頼できない値は変数として渡します。
expression = "//account[@tenant=$tenant and @id=$account_id]"
matches = root.xpath(
expression,
tenant="blue",
account_id=account_id,
)
if not matches:
return ("not found", 404)
return etree.tostring(matches[0], encoding="unicode")
変更前は account_id を引用符の内側に埋め込むため、入力によって条件の構造が変わる可能性があります。変更後は同じ値をXPath変数として渡すので、引用符や演算子が含まれていても式の構文として解釈されません。形式や長さの検証は業務上の制約として追加できますが、変数のバインドを置き換えるものではありません。
ElementTree
ユーザーが検索モードを選択する場合は、式全体を対応付けます。
python
import xml.etree.ElementTree as ET
from flask import request
def accounts_by_view():
account_paths = {
"active": ".//account[@active='true']",
"disabled": ".//account[@active='false']",
}
selected_path = account_paths.get(request.args.get("view"))
if selected_path is None:
return ("invalid view", 400)
root = ET.fromstring(XML_DATA)
return root.findall(selected_path)
適用時の注意点
許可リストと式の対応表はサーバー側で管理してください。XMLパーサーの外部エンティティ設定と入力サイズの制限も、XPath式の保護とは別に確認してください。