(Last Updated On: 2021年2月15日)

XPathクエリ(1)の続きです。

現在のPHPのXPathクエリの問題はXPath 1.0である事です。通常の仕様書であれば特殊文字のある文字列の場合はエスケープ方法やエスケープAPIが定義されています。エスケープ方法が定義されていなければ「特殊文字を入力することができない」からです。エスケープ方法が無い場合は「エスケープが必要ないAPI」を用意すべきです。

しかし、XPath 1.0ではエスケープ方法もエスケープAPI、エスケープが必要ないAPIも定義されていません。このような場合、仕様を実装しているライブラリの実際の挙動を確認して利用することになります。PHPのSimpleXML、XMLXpathクラスの場合はlibxml2を利用しているので、libxml2の動作を確かめる必要があります。

モジュール内部でエスケープを実装している場合もあるので、その挙動はライブラリのエスケープAPIと異なる場合が在ることにも注意が必要です。例えば、SQLite3は文字列のエスケープ方法は定義していますが、エスケープAPIは定義していません。PDO SQLite3のescape()メソッドはPDOのSQLite3ドライバーモジュールで定義しています。pgsql(PostgreSQL)モジュールは古いPostgreSQLのクライアントライブラリ(libpq)を利用した場合、接続情報を利用しマルチバイト文字をエスケープするPQescapeStringConnが無いため、接続情報を利用しないPQescapeStringを利用します。PQescapeLiteral、PQescapeIdentifierが無い場合、内部の実装を使う場合もあります。

PDO SQLite3ドライバーやpgsqlモジュールのエスケープ実装に大きな問題ある訳ではありませんが、情報を処理するシステム以外が提供したAPI以外のエスケープ実装を利用する際には注意が必要です。例えば、pgsqlモジュールをPQescapeStringConnがない古いlibpqとコンパイルするとSJISなどの文字エンコーディングをクライアント文字エンコーディングとして安全に利用できなくなる事があります。

XPath 1.0の場合、エスケープ方法は定義されていませんが文字リテラルのクオート方法が2種類あります。

ダブルクォート

"シングルクォート(’)も安全"

シングルクォート

'ダブルクォート(”)も安全'

XPath 1.0の仕様書に従った安全なクエリの実行は、文字列中にシングルクォートが無い場合はダブルクォートで囲み、ダブルクォートが無い場合はシングルクォートで囲めば安全であることが仕様的に保証されます。

ダブルクォートとシングルクォートの両方がある場合はどうするか?が問題になります。XPath 1.0には文字列連結関数concatが定義されています。この関数を使うと両方のクオート文字を含む文字列がある場合でも動作します。

<?php
$string = "
<books>
 <book>
  <author>Let</author>
  <title>PHP Security #2</title>
 </book>
 <book>
  <author>Let's</author>
  <title>PHP Security #1</title>
 </book>
 <book>
  <author>Let's \"PHP Security\"</author>
  <title>PHP Security #3</title>
 </book>
</books>
";

$xml = new SimpleXMLElement($string);
var_dump($xml->xpath('/books/book[author=concat("Let\'s", \' "PHP Security"\')]'));
?>

このコードを実行すると正しく3番目のノードが選択されます。

実行結果

array(1) {
  [0]=>
  object(SimpleXMLElement)#2 (2) {
    ["author"]=>
    string(20) "Let's "PHP Security""
    ["title"]=>
    string(15) "PHP Security #3"
  }
}

正しく実行できましたが

Let's "PHP Security"

をクエリする為にconcat()を利用して

var_dump($xml->xpath('/books/book[author=concat("let\'s", \' "PHP Security"\')]'));

とするのはとても面倒です。関数を作って対応する方が現実的です。

独自エスケープ関数版

<?php
$string = "
<books>
 <book>
  <author>Let</author>
  <title>PHP Security #2</title>
 </book>
 <book>
  <author>Let's</author>
  <title>PHP Security #1</title>
 </book>
 <book>
  <author>Let's \"PHP Security\"</author>
  <title>PHP Security #3</title>
 </book>
</books>
";

$xml = new SimpleXMLElement($string);

function xpath_escape_string($input) {
    if (false === strpos($input, "'")) {
        return "'$input'";
    }
    if (false === strpos($input, '"')) {
        return "\"$input\"";
    }
    return "concat('" . strtr($input, array("'" => '\', "\'", \'')) . "')";
}

$escaped = xpath_escape_string('Let\'s "PHP Security"');
var_dump($escaped);
var_dump($xml->xpath('/books/book[author='.$escaped.']'));
?>

これで一応、両方のクオート文字が利用できるようになりました。この関数は両方のクオート文字をエスケープするだけでなく、XMLのエンティティ(&apos; &quot;)、文字参照(&#34; &#39;)が利用された場合でも動作するようになります。

例えば、次のコードを実行すると次のエラーで<実行できない事が確認できます。

var_dump($xml->xpath('/books/book[author='.$escaped.']'));

発生するエラー

Warning: SimpleXMLElement::xpath(): Invalid expression in /home/yohgaki/t.php on line 34

Warning: SimpleXMLElement::xpath(): xmlXPathEval: evaluation failed in /home/yohgaki/t.php on line 34

xpath_escape_stringを利用した場合にはエラーは発生しません。XPath 1.0はエスケープ関数が無い為、エスケープしようとした場合にはこのようなおかしな操作が必要になります。

この他にも属性を使う方法もあります。この方法ではプリペアードクエリの様にエスケープ無しでXPathクエリを実行できますが、問題がない訳ではありません。

$xml['param']='Let\'s "PHP Security"'; // param属性を追加
var_dump($xml->xpath('/books/book[author=/*/@param]'));

これを追加して実行してもノードを取得できることが分かります。この方法の問題点は解りづらい事です。

$xml['param']='Let\'s "PHP Security"';
var_dump($xml->xpath('/books/book[author=@param]'));

を実行するとノードを取得できません。文字列で指定した場合と属性で指定した場合ではクエリの方法が変わります。”/*/@param”とする事が解かり易いと感じるユーザにはお薦めですが、一般にお薦めするのはどうかと思います。

直ぐにXPath 2.0に移行するのかと思っていましたが、XPath 1.0との互換性の無さの為になかなか進みません。恐らく当分の間はXPath 1.0を利用し続けなければならないようです。

PHPのセキュリティ入門書に記載するコンテンツのレビューも兼ねてブログを書いています。コメント、感想は大歓迎です。

参考リンク:

投稿者: yohgaki