xplist: Enforce single root value inside <plist>

Ensure that XML property lists contain exactly one root value inside the <plist> element and reject any additional value nodes before </plist>.

Add tests covering root value handling and nested CF$UID conversion behavior.

Co-authored-by: Sami Kortelainen <sami.kortelainen@piceasoft.com>
Co-authored-by: Nikias Bassen <nikias@gmx.li>
This commit is contained in:
Sami Kortelainen
2026-02-25 02:27:00 +01:00
committed by Nikias Bassen
parent f5e74fc1e0
commit 6e03a1df6d
5 changed files with 203 additions and 17 deletions
+10
View File
@@ -54,6 +54,7 @@ test/plist_btest
test/plist_jtest
test/plist_otest
test/integer_set_test
test/xml_behavior_test
test/data/*.out
test/*.trs
cython/Makefile
@@ -62,3 +63,12 @@ cython/.deps
cython/.libs
cython/plist.c
test-driver
# Generated test output files
test/data/*.test.bin
test/data/*.test.signed.bin
test/data/*.test.unsigned.bin
test/data/*.test.unsigned.xml
test/data/*.test.tz*.bin
test/data/*.test.tz*.xml
test/data/*.test.xml
+17 -15
View File
@@ -1170,8 +1170,9 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
ctx->pos++;
if (!strcmp(tag, "plist")) {
if (!node_path && *plist) {
/* we don't allow another top-level <plist> */
break;
PLIST_XML_ERR("Multiple top-level <plist> elements encountered\n");
ctx->err = PLIST_ERR_PARSE;
goto err_out;
}
if (is_empty) {
PLIST_XML_ERR("Empty plist tag\n");
@@ -1403,12 +1404,6 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
data->length = length;
}
} else {
if (!strcmp(tag, "key") && !keyname && parent && (plist_get_node_type(parent) == PLIST_DICT)) {
keyname = strdup("");
plist_free(subnode);
subnode = NULL;
continue;
}
data->strval = strdup("");
data->length = 0;
}
@@ -1501,14 +1496,15 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
}
if (subnode && !closing_tag) {
if (!*plist) {
/* first node, make this node the parent node */
/* first value node inside <plist> */
*plist = subnode;
if (data->type != PLIST_DICT && data->type != PLIST_ARRAY) {
/* if the first node is not a structered node, we're done */
subnode = NULL;
goto err_out;
if (data->type == PLIST_DICT || data->type == PLIST_ARRAY) {
parent = subnode;
} else {
/* scalar root: keep parsing until </plist> */
parent = NULL;
}
parent = subnode;
} else if (parent) {
switch (plist_get_node_type(parent)) {
case PLIST_DICT:
@@ -1528,6 +1524,11 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
ctx->err = PLIST_ERR_PARSE;
goto err_out;
}
} else {
/* We already produced root, and we're not inside a container */
PLIST_XML_ERR("Unexpected tag <%s> found while </plist> is expected\n", tag);
ctx->err = PLIST_ERR_PARSE;
goto err_out;
}
if (!is_empty && (data->type == PLIST_DICT || data->type == PLIST_ARRAY)) {
if (depth >= PLIST_MAX_NESTING_DEPTH) {
@@ -1547,6 +1548,8 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
depth++;
parent = subnode;
} else {
/* If we inserted a child scalar into a container, nothing to push. */
}
subnode = NULL;
}
@@ -1587,7 +1590,6 @@ handle_closing:
node_path = (struct node_path_item*)node_path->prev;
free(path_item);
parent = (parent) ? ((node_t)parent)->parent : NULL;
/* parent can be NULL when we just closed the root node; keep parsing */
}
free(keyname);
keyname = NULL;
+7 -2
View File
@@ -13,7 +13,8 @@ noinst_PROGRAMS = \
integer_set_test \
plist_btest \
plist_jtest \
plist_otest
plist_otest \
xml_behavior_test
plist_cmp_SOURCES = plist_cmp.c
plist_cmp_LDADD = \
@@ -38,6 +39,9 @@ plist_jtest_LDADD = $(top_builddir)/src/libplist-2.0.la
plist_otest_SOURCES = plist_otest.c
plist_otest_LDADD = $(top_builddir)/src/libplist-2.0.la
xml_behavior_test_SOURCES = xml_behavior_test.c
xml_behavior_test_LDADD = $(top_builddir)/src/libplist-2.0.la
TESTS = \
empty.test \
small.test \
@@ -79,7 +83,8 @@ TESTS = \
ostep2.test \
ostep-strings.test \
ostep-comments.test \
ostep-invalid-types.test
ostep-invalid-types.test \
xml_behavior.test
EXTRA_DIST = \
$(TESTS) \
+2
View File
@@ -0,0 +1,2 @@
## -*- sh -*-
$top_builddir/test/xml_behavior_test
+167
View File
@@ -0,0 +1,167 @@
/*
* xml_behavior_test.c
*
* Tests XML parser behavior for correctness and specification compliance:
*
* 1) A <plist> element must contain exactly one root value node.
* Any additional value nodes after the first root object must
* cause parsing to fail.
*
* 2) Dictionaries of the form:
* <dict>
* <key>CF$UID</key>
* <integer>...</integer>
* </dict>
* must be converted to PLIST_UID nodes during XML parsing,
* including when they appear nested inside other containers.
*
* These tests ensure proper root handling and UID node conversion
* when parsing XML property lists.
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include "plist/plist.h"
static int test_nested_cfuid_converts_to_uid(void)
{
const char *xml =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
"<plist version=\"1.0\">"
" <dict>"
" <key>obj</key>"
" <dict>"
" <key>CF$UID</key>"
" <integer>7</integer>"
" </dict>"
" </dict>"
"</plist>";
plist_t root = NULL;
plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
if (err != PLIST_ERR_SUCCESS || !root) {
fprintf(stderr, "nested CF$UID: plist_from_xml failed (err=%d)\n", err);
plist_free(root);
return 0;
}
if (plist_get_node_type(root) != PLIST_DICT) {
fprintf(stderr, "nested CF$UID: root is not dict\n");
plist_free(root);
return 0;
}
plist_t obj = plist_dict_get_item(root, "obj");
if (!obj) {
fprintf(stderr, "nested CF$UID: missing key 'obj'\n");
plist_free(root);
return 0;
}
if (plist_get_node_type(obj) != PLIST_UID) {
fprintf(stderr, "nested CF$UID: expected PLIST_UID, got %d\n",
plist_get_node_type(obj));
plist_free(root);
return 0;
}
uint64_t uid = 0;
plist_get_uid_val(obj, &uid);
if (uid != 7) {
fprintf(stderr, "nested CF$UID: expected uid=7, got %" PRIu64 "\n", uid);
plist_free(root);
return 0;
}
plist_free(root);
return 1;
}
static int test_extra_root_value_is_rejected(void)
{
/* Two root values inside <plist> must be rejected */
const char *xml =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<plist version=\"1.0\">"
" <string>one</string>"
" <string>two</string>"
"</plist>";
plist_t root = NULL;
plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
/* Must fail, and root must be NULL (consistent with other parsers) */
if (err == PLIST_ERR_SUCCESS || root != NULL) {
fprintf(stderr, "extra root value: expected failure, got err=%d root=%p\n",
err, (void*)root);
plist_free(root);
return 0;
}
return 1;
}
static int test_scalar_then_extra_node_is_rejected(void)
{
/* Scalar root followed by another node must be rejected */
const char *xml =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<plist version=\"1.0\">"
" <true/>"
" <dict><key>A</key><string>x</string></dict>"
"</plist>";
plist_t root = NULL;
plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
if (err == PLIST_ERR_SUCCESS || root != NULL) {
fprintf(stderr, "scalar then extra node: expected failure, got err=%d root=%p\n",
err, (void*)root);
plist_free(root);
return 0;
}
return 1;
}
static int test_scalar_with_comment_is_ok(void)
{
/* Comment after the single root value is not an extra value node */
const char *xml =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<plist version=\"1.0\">"
" <string>ok</string>"
" <!-- trailing comment -->"
"</plist>";
plist_t root = NULL;
plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
if (err != PLIST_ERR_SUCCESS || !root) {
fprintf(stderr, "scalar + comment: expected success, got err=%d\n", err);
plist_free(root);
return 0;
}
if (plist_get_node_type(root) != PLIST_STRING) {
fprintf(stderr, "scalar + comment: expected root string, got %d\n",
plist_get_node_type(root));
plist_free(root);
return 0;
}
plist_free(root);
return 1;
}
int main(void)
{
int ok = 1;
ok &= test_nested_cfuid_converts_to_uid();
ok &= test_extra_root_value_is_rejected();
ok &= test_scalar_then_extra_node_is_rejected();
ok &= test_scalar_with_comment_is_ok();
return ok ? 0 : 1;
}