14.11 XML 函数
本节讨论 MySQL 中的 XML 和相关功能。
可以使用 mysql 和 mysqldump 客户端以获取 MySQL 中的 XML 格式输出。见 第 6.5.1 节,“mysql — MySQL 命令行客户端” 和 第 6.5.4 节,“mysqldump — 数据库备份程序”。
MySQL 提供了两个基本 XPath 1.0 函数,可以提供基本 XPath 1.0 功能。XPath 1.0 标准的基本信息将在本节后面提供;然而,对于 XPath 1.0 的深入讨论超出了本手册的范围,您可以查看 XPath 1.0 标准以获取详细信息。对于 XPath 新手或需要复习基本知识的人,可以查看 Zvon.org XPath 教程,该教程可在多种语言中访问。
这些函数仍在开发中。我们将继续改进这些和其他 XML 和 XPath 功能在 MySQL 8.4 及更高版本中。您可以在 MySQL XML 用户论坛 中讨论这些函数,提出问题,获取帮助。
XPath 表达式支持用户变量和本地存储程序变量。用户变量弱检查;本地存储程序变量强检查(见也 Bug #26518):
-
用户变量(弱检查)。 使用语法
$@(即用户变量)不进行检查。服务器不发出警告或错误,如果变量类型错误或之前未被赋值。用户完全负责任何拼写错误,因为不发出警告。variable_name示例:
mysql> SET @xml = '<a><b>X</b><b>Y</b></a>'; Query OK, 0 rows affected (0.00 sec) mysql> SET @i =1, @j = 2; Query OK, 0 rows affected (0.00 sec) mysql> SELECT @i, ExtractValue(@xml, '//b[$@i]'); +------+--------------------------------+ | @i | ExtractValue(@xml, '//b[$@i]') | +------+--------------------------------+ | 1 | X | +------+--------------------------------+ 1 row in set (0.00 sec) mysql> SELECT @j, ExtractValue(@xml, '//b[$@j]'); +------+--------------------------------+ | @j | ExtractValue(@xml, '//b[$@j]') | +------+--------------------------------+ | 2 | Y | +------+--------------------------------+ 1 row in set (0.00 sec) mysql> SELECT @k, ExtractValue(@xml, '//b[$@k]'); +------+--------------------------------+ | @k | ExtractValue(@xml, '//b[$@k]') | +------+--------------------------------+ | NULL | | +------+--------------------------------+ 1 row in set (0.00 sec) -
存储程序变量(强检查)。 使用语法
$可以在存储程序中声明和使用这些函数。这些变量是本地存储程序的局部变量,并且强检查类型和值。variable_name示例:
mysql> DELIMITER | mysql> CREATE PROCEDURE myproc () -> BEGIN -> DECLARE i INT DEFAULT 1; -> DECLARE xml VARCHAR(25) DEFAULT '<a>X</a><a>Y</a><a>Z</a>'; -> -> WHILE i < 4 DO -> SELECT xml, i, ExtractValue(xml, '//a[$i]'); -> SET i = i+1; -> END WHILE; -> END | Query OK, 0 rows affected (0.01 sec) mysql> DELIMITER ; mysql> CALL myproc(); +--------------------------+---+------------------------------+ | xml | i | ExtractValue(xml, '//a[$i]') | +--------------------------+---+------------------------------+ | <a>X</a><a>Y</a><a>Z</a> | 1 | X | +--------------------------+---+------------------------------+ 1 row in set (0.00 sec) +--------------------------+---+------------------------------+ | xml | i | ExtractValue(xml, '//a[$i]') | +--------------------------+---+------------------------------+ | <a>X</a><a>Y</a><a>Z</a> | 2 | Y | +--------------------------+---+------------------------------+ 1 row in set (0.01 sec) +--------------------------+---+------------------------------+ | xml | i | ExtractValue(xml, '//a[$i]') | +--------------------------+---+------------------------------+ | <a>X</a><a>Y</a><a>Z</a> | 3 | Z | +--------------------------+---+------------------------------+ 1 row in set (0.01 sec)参数. 在存储程序中传递的参数中使用的变量也将受到强检查。
包含用户变量或存储程序中的局部变量的表达式,除非使用符号,否则必须遵守XPath 1.0规范中变量的规则。
将用户变量用作XPath表达式的存储变量将被视为空字符串。因此,无法将XPath表达式存储为用户变量。 (Bug #32911)
-
ExtractValue(xml_frag,xpath_expr)ExtractValue()接受两个字符串参数,一个XML标记片断xml_frag和一个XPath表达式xpath_expr(也称为定位符);它返回第一个匹配XPath表达式的元素或元素的第一个文本节点的文本内容(CDATA)。使用这个函数等同于执行使用
xpath_expr的匹配操作,并将其追加到/text()。换言之,ExtractValue('<a><b>Sakila</b></a>', '/a/b')和ExtractValue('<a><b>Sakila</b></a>', '/a/b/text()')将产生相同的结果。如果xml_frag或xpath_expr是NULL,函数将返回NULL。如果找到多个匹配项,函数将返回所有匹配项的第一个文本节点的内容(以匹配的顺序)作为一个单个的空格分隔字符串。
如果XPath表达式(包括隐式
/text())没有找到匹配的文本节点—for whatever reason—as long asxpath_expr是有效的,并且xml_frag由正确地嵌套和关闭的元素组成—an empty string will be returned. No distinction is made between a match on an empty element and no match at all. This is by design.如果你需要确定在
xml_frag中是否找到匹配的元素或找到匹配的元素但没有子文本节点,你应该测试使用XPathcount()函数的表达式。例如,这两个语句将返回空字符串,如下所示:mysql> SELECT ExtractValue('<a><b/></a>', '/a/b'); +-------------------------------------+ | ExtractValue('<a><b/></a>', '/a/b') | +-------------------------------------+ | | +-------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT ExtractValue('<a><c/></a>', '/a/b'); +-------------------------------------+ | ExtractValue('<a><c/></a>', '/a/b') | +-------------------------------------+ | | +-------------------------------------+ 1 row in set (0.00 sec)然而,可以确定是否实际找到匹配的元素使用以下语句:
mysql> SELECT ExtractValue('<a><b/></a>', 'count(/a/b)'); +-------------------------------------+ | ExtractValue('<a><b/></a>', 'count(/a/b)') | +-------------------------------------+ | 1 | +-------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT ExtractValue('<a><c/></a>', 'count(/a/b)'); +-------------------------------------+ | ExtractValue('<a><c/></a>', 'count(/a/b)') | +-------------------------------------+ | 0 | +-------------------------------------+ 1 row in set (0.01 sec)ImportantExtractValue()只返回CDATA,而不返回可能包含在匹配标记中的标记或标记的内容(见以下示例中返回的val1结果)。mysql> SELECT -> ExtractValue('<a>ccc<b>ddd</b></a>', '/a') AS val1, -> ExtractValue('<a>ccc<b>ddd</b></a>', '/a/b') AS val2, -> ExtractValue('<a>ccc<b>ddd</b></a>', '//b') AS val3, -> ExtractValue('<a>ccc<b>ddd</b></a>', '/b') AS val4, -> ExtractValue('<a>ccc<b>ddd</b><b>eee</b></a>', '//b') AS val5; +------+------+------+------+---------+ | val1 | val2 | val3 | val4 | val5 | +------+------+------+------+---------+ | ccc | ddd | ddd | | ddd eee | +------+------+------+------+---------+该函数使用当前SQL排序规则对
contains()进行比较,执行与其他字符串函数(例如CONCAT())相同的排序聚合操作,在考虑其参数的排序强制性;请参阅第12.8.4节,“排序强制性在表达式中”,了解相关规则。(之前,二进制—that is,大小写敏感—比较总是使用的。)
NULL如果xml_frag包含不正确地嵌套或关闭的元素,并生成警告,如下所示:mysql> SELECT ExtractValue('<a>c</a><b', '//a'); +-----------------------------------+ | ExtractValue('<a>c</a><b', '//a') | +-----------------------------------+ | NULL | +-----------------------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SHOW WARNINGS\G *************************** 1. row *************************** Level: Warning Code: 1525 Message: Incorrect XML value: 'parse error at line 1 pos 11: END-OF-INPUT unexpected ('>' wanted)' 1 row in set (0.00 sec) mysql> SELECT ExtractValue('<a>c</a><b/>', '//a'); +-------------------------------------+ | ExtractValue('<a>c</a><b/>', '//a') | +-------------------------------------+ | c | +-------------------------------------+ 1 row in set (0.00 sec) -
UpdateXML(xml_target,xpath_expr,new_xml)该函数将给定的XML标记片段
xml_target中的某个部分替换为新的XML片段new_xml,然后返回更改后的XML。该部分xml_target与用户提供的XPath表达式xpath_expr匹配。如果找不到匹配的表达式或找到多个匹配项,该函数返回原始
xml_targetXML片段。所有三个参数都应该是字符串。如果UpdateXML()的任何参数是NULL,该函数返回NULL。mysql> SELECT -> UpdateXML('<a><b>ccc</b><d></d></a>', '/a', '<e>fff</e>') AS val1, -> UpdateXML('<a><b>ccc</b><d></d></a>', '/b', '<e>fff</e>') AS val2, -> UpdateXML('<a><b>ccc</b><d></d></a>', '//b', '<e>fff</e>') AS val3, -> UpdateXML('<a><b>ccc</b><d></d></a>', '/a/d', '<e>fff</e>') AS val4, -> UpdateXML('<a><d></d><b>ccc</b><d></d></a>', '/a/d', '<e>fff</e>') AS val5 -> \G *************************** 1. row *************************** val1: <e>fff</e> val2: <a><b>ccc</b><d></d></a> val3: <a><e>fff</e><d></d></a> val4: <a><b>ccc</b><e>fff</e></a> val5: <a><d></d><b>ccc</b><d></d></a>
XPath语法和使用的深入讨论超出了本手册的范围。请查看XML Path Language (XPath) 1.0 specification以获取详细信息。对于XPath新手或需要复习基本知识的人,可以查看Zvon.org XPath Tutorial,该教程可在多种语言中访问。
以下是基本XPath表达式的描述和示例:
-
/tag匹配
<如果和仅当tag/><是根元素。tag/>示例:
/a在<a><b/></a>中匹配,因为它匹配最外层(根)标签。它不匹配<b><a/></b>中的内嵌a元素,因为在这个例子中,它是另一个元素的子元素。 -
/tag1/tag2匹配
<如果它是tag2/><的子元素,并且tag1/><是根元素。tag1/>示例:
/a/b匹配 XML 片段<a><b/></a>中的b元素,因为它是根元素a的子元素。它不在<b><a/></b>中,因为在这里b是根元素(因此不是其他元素的子元素)。这个构造可以扩展到三个或更多的元素。例如,XPath 表达式
/a/b/c匹配 XML 片段<a><b><c/></b></a>中的c元素。 -
//tag匹配任何
<的实例。tag>示例:
//a匹配以下任意一个:<a><b><c/></b></a>、<c><a><b/></a></b>、<c><b><a/></b></c>。//可以与/结合使用。例如,//a/b匹配以下任意一个:<a><b/></a>或<c><a><b/></a></c>。Note//等同于tag/descendant-or-self::*/。常见的错误是混淆这两个表达式,虽然后者表达式实际上可以导致非常不同的结果,如下所示:tagmysql> SET @xml = '<a><b><c>w</c><b>x</b><d>y</d>z</b></a>'; Query OK, 0 rows affected (0.00 sec) mysql> SELECT @xml; +-----------------------------------------+ | @xml | +-----------------------------------------+ | <a><b><c>w</c><b>x</b><d>y</d>z</b></a> | +-----------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT ExtractValue(@xml, '//b[1]'); +------------------------------+ | ExtractValue(@xml, '//b[1]') | +------------------------------+ | x z | +------------------------------+ 1 row in set (0.00 sec) mysql> SELECT ExtractValue(@xml, '//b[2]'); +------------------------------+ | ExtractValue(@xml, '//b[2]') | +------------------------------+ | | +------------------------------+ 1 row in set (0.01 sec) mysql> SELECT ExtractValue(@xml, '/descendant-or-self::*/b[1]'); +---------------------------------------------------+ | ExtractValue(@xml, '/descendant-or-self::*/b[1]') | +---------------------------------------------------+ | x z | +---------------------------------------------------+ 1 row in set (0.06 sec) mysql> SELECT ExtractValue(@xml, '/descendant-or-self::*/b[2]'); +---------------------------------------------------+ | ExtractValue(@xml, '/descendant-or-self::*/b[2]') | +---------------------------------------------------+ | | +---------------------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT ExtractValue(@xml, '/descendant-or-self::b[1]'); +-------------------------------------------------+ | ExtractValue(@xml, '/descendant-or-self::b[1]') | +-------------------------------------------------+ | z | +-------------------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT ExtractValue(@xml, '/descendant-or-self::b[2]'); +-------------------------------------------------+ | ExtractValue(@xml, '/descendant-or-self::b[2]') | +-------------------------------------------------+ | x | +-------------------------------------------------+ 1 row in set (0.00 sec) -
星号操作符(
*)作为““wildcard”,匹配任何元素。例如,表达式/*/b匹配 XML 片段<a><b/></a>或<c><b/></c>中的b元素。但是,表达式不在<b><a/></b>中,因为b必须是其他元素的子元素。星号可以在任何位置使用:表达式/*/b/*匹配b元素的任何子元素,但不是根元素。 -
您可以使用
|(UNION)操作符来匹配多个定位符。例如,表达式//b|//c匹配 XML 目标中的所有b和c元素。 -
还可以根据一个或多个属性的值来匹配一个元素。这可以使用语法
来实现。例如,表达式tag[@attribute="value"]//b[@id="idB"]将匹配碎片<a><b id="idA"/><c/><b id="idB"/></a>中的第二个b元素。要匹配具有多个属性值的元素,可以简单地使用多个属性比较子句。例如,表达式
//b[@c="x"][@d="y"]将匹配元素<b c="x" d="y"/>在给定的XML片段中出现。要找到具有相同属性匹配任意多个值的元素,可以使用多个定位符连接的|操作符。例如,要匹配所有
b元素,其中c属性具有值23或17,可以使用表达式//b[@c="23"]|//b[@c="17"]。您也可以使用逻辑操作符or://b[@c="23" or @c="17"]。Note在
or和|之间的区别是,or连接条件,而|连接结果集。
XPath限制。支持的XPath语法目前受到以下限制:
-
节点集到节点集比较(例如
'/a/b[@c=@d]')不受支持。 -
所有标准XPath比较操作符都支持。(Bug #22823)
-
相对定位器表达式将在根节点上解析。例如,考虑以下查询和结果:
mysql> SELECT ExtractValue( -> '<a><b c="1">X</b><b c="2">Y</b></a>', -> 'a/b' -> ) AS result; +--------+ | result | +--------+ | X Y | +--------+ 1 row in set (0.03 sec)在这种情况下,定位器
a/b将解析为/a/b。相对定位器也支持在谓词中使用。在以下示例中,
d[../@c="1"]将解析为/a/b[@c="1"]/d:mysql> SELECT ExtractValue( -> '<a> -> <b c="1"><d>X</d></b> -> <b c="2"><d>X</d></b> -> </a>', -> 'a/b/d[../@c="1"]') -> AS result; +--------+ | result | +--------+ | X | +--------+ 1 row in set (0.00 sec) -
不允许使用表达式评估为标量值的定位器,包括变量引用、字面值、数字和标量函数调用,这将导致错误。
-
::操作符不支持与节点类型的组合,例如:-
axis::comment() -
axis::text() -
axis::processing-instructions() -
axis::node()
然而,名称测试(例如
和axis::name)是支持的,如下所示:axis::*mysql> SELECT ExtractValue('<a><b>x</b><c>y</c></a>','/a/child::b'); +-------------------------------------------------------+ | ExtractValue('<a><b>x</b><c>y</c></a>','/a/child::b') | +-------------------------------------------------------+ | x | +-------------------------------------------------------+ 1 row in set (0.02 sec) mysql> SELECT ExtractValue('<a><b>x</b><c>y</c></a>','/a/child::*'); +-------------------------------------------------------+ | ExtractValue('<a><b>x</b><c>y</c></a>','/a/child::*') | +-------------------------------------------------------+ | x y | +-------------------------------------------------------+ 1 row in set (0.01 sec) -
-
“上下”导航在路径将指向根元素的祖先时不受支持。这意味着,您不能使用表达式来匹配祖先元素的后代,其中祖先元素中至少有一个也是根元素的祖先(见Bug #16321)。
-
以下XPath函数不受支持或存在已知问题,如下所示:
-
id() -
lang() -
local-name() -
name() -
namespace-uri() -
normalize-space() -
starts-with() -
string() -
substring-after() -
substring-before() -
translate()
-
-
以下轴不受支持:
-
following-sibling -
following -
preceding-sibling -
preceding
-
XPath表达式作为ExtractValue()和UpdateXML()的参数可能包含元素选择器中的冒号字符(:),从而启用使用XML命名空间notation的标记。例如:
mysql> SET @xml = '<a>111<b:c>222<d>333</d><e:f>444</e:f></b:c></a>';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT ExtractValue(@xml, '//e:f');
+-----------------------------+
| ExtractValue(@xml, '//e:f') |
+-----------------------------+
| 444 |
+-----------------------------+
1 row in set (0.00 sec)
mysql> SELECT UpdateXML(@xml, '//b:c', '<g:h>555</g:h>');
+--------------------------------------------+
| UpdateXML(@xml, '//b:c', '<g:h>555</g:h>') |
+--------------------------------------------+
| <a>111<g:h>555</g:h></a> |
+--------------------------------------------+
1 row in set (0.00 sec)
这与Apache Xalan和一些其他解析器所允许的类似,并且比需要命名空间声明或使用namespace-uri()和local-name()函数要简单得多。
Error handling. 对于ExtractValue()和UpdateXML(),XPath定位符必须是有效的,XML要搜索的元素必须是正确地嵌套和关闭。如果定位符无效,错误将被生成:
mysql> SELECT ExtractValue('<a>c</a><b/>', '/&a');
ERROR 1105 (HY000): XPATH syntax error: '&a'
如果xml_frag不包含正确地嵌套和关闭的元素,NULL将被返回,并且警告将被生成,如下所示:
mysql> SELECT ExtractValue('<a>c</a><b', '//a');
+-----------------------------------+
| ExtractValue('<a>c</a><b', '//a') |
+-----------------------------------+
| NULL |
+-----------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
Level: Warning
Code: 1525
Message: Incorrect XML value: 'parse error at line 1 pos 11:
END-OF-INPUT unexpected ('>' wanted)'
1 row in set (0.00 sec)
mysql> SELECT ExtractValue('<a>c</a><b/>', '//a');
+-------------------------------------+
| ExtractValue('<a>c</a><b/>', '//a') |
+-------------------------------------+
| c |
+-------------------------------------+
1 row in set (0.00 sec)
UpdateXML()的第三个参数replacement XML不检查是否仅包含正确地嵌套和关闭的元素。
XPath Injection. 代码注入是指在系统中引入恶意代码以获取未经授权的访问权限和数据。它基于开发者对用户输入数据类型和内容的假设。XPath也不例外。
一个常见的场景是应用程序通过匹配登录名和密码与XML文件中的相应项来进行授权,使用XPath表达式如下:
//user[login/text()='neapolitan' and password/text()='1c3cr34m']/attribute::id
这与SQL语句类似:
SELECT id FROM users WHERE login='neapolitan' AND password='1c3cr34m';
使用 XPath 的 PHP 应用程序可能会像这样处理登录过程:
<?php
$file = "users.xml";
$login = $POST["login"];
$password = $POST["password"];
$xpath = "//user[login/text()=$login and password/text()=$password]/attribute::id";
if( file_exists($file) )
{
$xml = simplexml_load_file($file);
if($result = $xml->xpath($xpath))
echo "You are now logged in as user $result[0].";
else
echo "Invalid login name or password.";
}
else
exit("Failed to open $file.");
?>
没有对输入进行检查。这意味着恶意用户可以通过输入 ' or 1=1 作为登录名和密码来“短路”测试,导致 $xpath 被评估为以下内容:
//user[login/text()='' or 1=1 and password/text()='' or 1=1]/attribute::id
由于方括号中的表达式总是评估为 true,因此实际上等同于这个匹配 XML 文件中的每个 user 元素的 id 属性的表达式:
//user/attribute::id
可以通过将变量名在定义 $xpath 时用引号括起来,强制将来自 Web 表单的值转换为字符串来规避这个攻击:
$xpath = "//user[login/text()='$login' and password/text()='$password']/attribute::id";
这与防止 SQL 注入攻击的策略相同。在一般情况下,防止 XPath 注入攻击的实践方法与防止 SQL 注入攻击的实践方法相同:
-
从未接受未经测试的用户数据在应用程序中。
-
检查所有用户提交的数据类型;拒绝或转换类型不正确的数据。
-
对数字数据进行范围检查;截断、四舍五入或拒绝超出范围的值。测试字符串是否包含非法字符,并将其删除或拒绝包含非法字符的输入。
-
不要输出明确的错误消息,这可能会向未经授权的用户提供泄露系统的线索;将这些错误消息记录到文件或数据库表中。
正如 SQL 注入攻击可以用来获取数据库架构信息一样,XPath 注入攻击也可以用来遍历 XML 文件以 uncover它们的结构,如 Amit Klein 的论文 Blind XPath Injection(PDF 文件,46KB)中所讨论的那样。
还需要检查回传到客户端的输出。考虑使用 MySQL ExtractValue() 函数时的情况:
mysql> SELECT ExtractValue(
-> LOAD_FILE('users.xml'),
-> '//user[login/text()="" or 1=1 and password/text()="" or 1=1]/attribute::id'
-> ) AS id;
+-------------------------------+
| id |
+-------------------------------+
| 00327 13579 02403 42354 28570 |
+-------------------------------+
1 row in set (0.01 sec)
因为 ExtractValue() 函数返回多个匹配结果作为一个空格分隔的字符串,这个注入攻击将将 users.xml 中的每个有效 ID 发送回用户作为一个输出行。作为额外的安全措施,您也应该在返回输出前对其进行测试。以下是一个简单的示例:
mysql> SELECT @id = ExtractValue(
-> LOAD_FILE('users.xml'),
-> '//user[login/text()="" or 1=1 and password/text()="" or 1=1]/attribute::id'
-> );
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT IF(
-> INSTR(@id, ' ') = 0,
-> @id,
-> 'Unable to retrieve user ID')
-> AS singleID;
+----------------------------+
| singleID |
+----------------------------+
| Unable to retrieve user ID |
+----------------------------+
1 row in set (0.00 sec)
总的来说,返回数据给用户的安全指南与接受用户输入的指南相同,可以总结为:
-
总是对出站数据进行类型和可接受值的检查。
-
从不允许未经授权的用户查看可能会泄露应用程序信息的错误消息。