Skip to content

Commit 30885f3

Browse files
authored
Implement request #71571: XSLT processor should provide option to change maxDepth (#13731)
There are two depth limiting parameters for XSLT templates. 1) maxTemplateDepth This corresponds to the recursion depth of a template. For very complicated templates this can be hit. 2) maxTemplateVars This is the total number of live variables. When using recursive templates with lots of parameters you can hit this limit. This patch introduces two new properties to XSLTProcessor that corresponds to the above variables.
1 parent 089f513 commit 30885f3

18 files changed

+532
-14
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,7 @@ PHP NEWS
236236
. Implement request #64137 (XSLTProcessor::setParameter() should allow both
237237
quotes to be used). (nielsdos)
238238
. Implemented "Improve callbacks in ext/dom and ext/xsl" RFC. (nielsdos)
239+
. Added XSLTProcessor::$maxTemplateDepth and XSLTProcessor::$maxTemplateVars.
240+
(nielsdos)
239241

240242
<<< NOTE: Insert NEWS from last stable release here prior to actual release! >>>

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ PHP 8.4 UPGRADE NOTES
261261
quotes.
262262
. It is now possible to pass any callable to registerPhpFunctions().
263263
RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl
264+
. Added XSLTProcessor::$maxTemplateDepth and XSLTProcessor::$maxTemplateVars
265+
to control the recursion depth of XSL template evaluation.
264266

265267
========================================
266268
3. Changes in SAPI modules

UPGRADING.INTERNALS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ PHP 8.4 INTERNALS UPGRADE NOTES
192192
d. ext/libxml
193193
- Added php_libxml_pretend_ctx_error_ex() to emit errors as if they had come
194194
from libxml.
195+
- Added php_libxml_error_handler_va() to pass libxml errors, and
196+
corresponding php_libxml_error_level enum.
195197
- Removed the "properties" HashTable field from php_libxml_node_object.
196198
- Added a way to attached private data to a php_libxml_ref_obj.
197199
- Added a way to fix a class type onto php_libxml_ref_obj.

ext/dom/xpath_callbacks.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserC
6767
xmlXPathFreeObject(obj);
6868
}
6969

70-
/* Push sentinel value */
71-
valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
70+
/* Don't push a sentinel value here. If this is called from an error situation, then by *not* pushing a sentinel
71+
* the execution will halt. If this is called from a regular situation, then it is the caller's responsibility
72+
* to ensure the stack remains balanced. */
7273
}
7374

7475
PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *registry)

ext/libxml/libxml.c

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@
4545
#include "php_libxml.h"
4646

4747
#define PHP_LIBXML_LOADED_VERSION ((char *)xmlParserVersion)
48-
#define PHP_LIBXML_ERROR 0
49-
#define PHP_LIBXML_CTX_ERROR 1
50-
#define PHP_LIBXML_CTX_WARNING 2
5148

5249
#include "libxml_arginfo.h"
5350

@@ -647,12 +644,12 @@ void php_libxml_issue_error(int level, const char *msg)
647644
}
648645
}
649646

650-
static void php_libxml_internal_error_handler_ex(int error_type, void *ctx, const char **msg, va_list ap, int line, int column)
647+
static void php_libxml_internal_error_handler_ex(php_libxml_error_level error_type, void *ctx, const char *msg, va_list ap, int line, int column)
651648
{
652649
char *buf;
653650
int len, len_iter, output = 0;
654651

655-
len = vspprintf(&buf, 0, *msg, ap);
652+
len = vspprintf(&buf, 0, msg, ap);
656653
len_iter = len;
657654

658655
/* remove any trailing \n */
@@ -685,7 +682,7 @@ static void php_libxml_internal_error_handler_ex(int error_type, void *ctx, cons
685682
}
686683
}
687684

688-
static void php_libxml_internal_error_handler(int error_type, void *ctx, const char **msg, va_list ap)
685+
PHP_LIBXML_API void php_libxml_error_handler_va(php_libxml_error_level error_type, void *ctx, const char *msg, va_list ap)
689686
{
690687
int line = 0;
691688
int column = 0;
@@ -831,7 +828,7 @@ PHP_LIBXML_API void php_libxml_pretend_ctx_error_ex(const char *file, int line,
831828
{
832829
va_list args;
833830
va_start(args, msg);
834-
php_libxml_internal_error_handler_ex(PHP_LIBXML_CTX_ERROR, NULL, &msg, args, line, column);
831+
php_libxml_internal_error_handler_ex(PHP_LIBXML_CTX_ERROR, NULL, msg, args, line, column);
835832
va_end(args);
836833

837834
/* Propagate back into libxml */
@@ -853,15 +850,15 @@ PHP_LIBXML_API void php_libxml_ctx_error(void *ctx, const char *msg, ...)
853850
{
854851
va_list args;
855852
va_start(args, msg);
856-
php_libxml_internal_error_handler(PHP_LIBXML_CTX_ERROR, ctx, &msg, args);
853+
php_libxml_error_handler_va(PHP_LIBXML_CTX_ERROR, ctx, msg, args);
857854
va_end(args);
858855
}
859856

860857
PHP_LIBXML_API void php_libxml_ctx_warning(void *ctx, const char *msg, ...)
861858
{
862859
va_list args;
863860
va_start(args, msg);
864-
php_libxml_internal_error_handler(PHP_LIBXML_CTX_WARNING, ctx, &msg, args);
861+
php_libxml_error_handler_va(PHP_LIBXML_CTX_WARNING, ctx, msg, args);
865862
va_end(args);
866863
}
867864

@@ -878,7 +875,7 @@ PHP_LIBXML_API void php_libxml_error_handler(void *ctx, const char *msg, ...)
878875
{
879876
va_list args;
880877
va_start(args, msg);
881-
php_libxml_internal_error_handler(PHP_LIBXML_ERROR, ctx, &msg, args);
878+
php_libxml_error_handler_va(PHP_LIBXML_ERROR, ctx, msg, args);
882879
va_end(args);
883880
}
884881

ext/libxml/php_libxml.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ static zend_always_inline void php_libxml_invalidate_node_list_cache_from_doc(xm
141141

142142
typedef void * (*php_libxml_export_node) (zval *object);
143143

144+
typedef enum {
145+
PHP_LIBXML_ERROR = 0,
146+
PHP_LIBXML_CTX_ERROR = 1,
147+
PHP_LIBXML_CTX_WARNING = 2,
148+
} php_libxml_error_level;
149+
144150
PHP_LIBXML_API int php_libxml_increment_node_ptr(php_libxml_node_object *object, xmlNodePtr node, void *private_data);
145151
PHP_LIBXML_API int php_libxml_decrement_node_ptr(php_libxml_node_object *object);
146152
PHP_LIBXML_API int php_libxml_increment_doc_ref(php_libxml_node_object *object, xmlDocPtr docp);
@@ -157,6 +163,7 @@ PHP_LIBXML_API void php_libxml_error_handler(void *ctx, const char *msg, ...);
157163
PHP_LIBXML_API void php_libxml_ctx_warning(void *ctx, const char *msg, ...);
158164
PHP_LIBXML_API void php_libxml_pretend_ctx_error_ex(const char *file, int line, int column, const char *msg,...);
159165
PHP_LIBXML_API void php_libxml_ctx_error(void *ctx, const char *msg, ...);
166+
PHP_LIBXML_API void php_libxml_error_handler_va(php_libxml_error_level error_type, void *ctx, const char *msg, va_list args);
160167
PHP_LIBXML_API int php_libxml_xmlCheckUTF8(const unsigned char *s);
161168
PHP_LIBXML_API void php_libxml_switch_context(zval *context, zval *oldcontext);
162169
PHP_LIBXML_API void php_libxml_issue_error(int level, const char *msg);

ext/xsl/php_xsl.c

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,152 @@ zend_object *xsl_objects_new(zend_class_entry *class_type)
118118
intern->parameter = zend_new_array(0);
119119
php_dom_xpath_callbacks_ctor(&intern->xpath_callbacks);
120120

121+
/* Default initialize properties that could not be default initialized at the stub because they depend on library
122+
* configuration parameters. */
123+
ZVAL_LONG(xsl_prop_max_template_depth(&intern->std), xsltMaxDepth);
124+
ZVAL_LONG(xsl_prop_max_template_vars(&intern->std), xsltMaxVars);
125+
121126
return &intern->std;
122127
}
123128
/* }}} */
124129

130+
#if ZEND_DEBUG
131+
# define XSL_DEFINE_PROP_ACCESSOR(c_name, php_name, prop_index) \
132+
zval *xsl_prop_##c_name(zend_object *object) \
133+
{ \
134+
zend_string *prop_name = ZSTR_INIT_LITERAL(php_name, false); \
135+
const zend_property_info *prop_info = zend_get_property_info(xsl_xsltprocessor_class_entry, prop_name, 0); \
136+
zend_string_release_ex(prop_name, false); \
137+
ZEND_ASSERT(OBJ_PROP_TO_NUM(prop_info->offset) == prop_index); \
138+
return OBJ_PROP_NUM(object, prop_index); \
139+
}
140+
#else
141+
# define XSL_DEFINE_PROP_ACCESSOR(c_name, php_name, prop_index) \
142+
zval *xsl_prop_##c_name(zend_object *object) \
143+
{ \
144+
return OBJ_PROP_NUM(object, prop_index); \
145+
}
146+
#endif
147+
148+
XSL_DEFINE_PROP_ACCESSOR(max_template_depth, "maxTemplateDepth", 2)
149+
XSL_DEFINE_PROP_ACCESSOR(max_template_vars, "maxTemplateVars", 3)
150+
151+
static zval *xsl_objects_write_property_with_validation(zend_object *object, zend_string *member, zval *value, void **cache_slot, zval *property)
152+
{
153+
/* Read old value so we can restore it if necessary. The value is not refcounted as its type is IS_LONG. */
154+
ZEND_ASSERT(Z_TYPE_P(property) == IS_LONG);
155+
zend_long old_property_value = Z_LVAL_P(property);
156+
157+
/* Write new property, which will also potentially perform coercions. */
158+
zend_std_write_property(object, member, value, NULL);
159+
160+
/* Validate value *after* coercions have been performed, and restore the old value if necessary. */
161+
if (UNEXPECTED(Z_LVAL_P(property) < 0)) {
162+
Z_LVAL_P(property) = old_property_value;
163+
zend_value_error("%s::$%s must be greater than or equal to 0", ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
164+
return &EG(error_zval);
165+
}
166+
167+
return property;
168+
}
169+
170+
static zval *xsl_objects_write_property(zend_object *object, zend_string *member, zval *value, void **cache_slot)
171+
{
172+
/* Extra validation for maxTemplateDepth and maxTemplateVars */
173+
if (zend_string_equals_literal(member, "maxTemplateDepth")) {
174+
zval *property = xsl_prop_max_template_depth(object);
175+
return xsl_objects_write_property_with_validation(object, member, value, cache_slot, property);
176+
} else if (zend_string_equals_literal(member, "maxTemplateVars")) {
177+
zval *property = xsl_prop_max_template_vars(object);
178+
return xsl_objects_write_property_with_validation(object, member, value, cache_slot, property);
179+
} else {
180+
return zend_std_write_property(object, member, value, cache_slot);
181+
}
182+
}
183+
184+
static bool xsl_is_validated_property(const zend_string *member)
185+
{
186+
return zend_string_equals_literal(member, "maxTemplateDepth") || zend_string_equals_literal(member, "maxTemplateVars");
187+
}
188+
189+
static zval *xsl_objects_get_property_ptr_ptr(zend_object *object, zend_string *member, int type, void **cache_slot)
190+
{
191+
if (xsl_is_validated_property(member)) {
192+
return NULL;
193+
}
194+
195+
return zend_std_get_property_ptr_ptr(object, member, type, cache_slot);
196+
}
197+
198+
static zval *xsl_objects_read_property(zend_object *object, zend_string *member, int type, void **cache_slot, zval *rv)
199+
{
200+
/* read handler is being called as a fallback after get_property_ptr_ptr returned NULL */
201+
if (type != BP_VAR_IS && type != BP_VAR_R && xsl_is_validated_property(member)) {
202+
zend_throw_error(NULL, "Indirect modification of %s::$%s is not allowed", ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
203+
return &EG(uninitialized_zval);
204+
}
205+
206+
return zend_std_read_property(object, member, type, cache_slot, rv);
207+
}
208+
209+
static void xsl_objects_unset_property(zend_object *object, zend_string *member, void **cache_slot)
210+
{
211+
if (xsl_is_validated_property(member)) {
212+
zend_throw_error(NULL, "Cannot unset %s::$%s", ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
213+
return;
214+
}
215+
216+
zend_std_unset_property(object, member, cache_slot);
217+
}
218+
219+
/* Tries to output an error message where a part was replaced by another string.
220+
* Returns true if the search string was found and the error message with replacement was outputted.
221+
* Return false otherwise. */
222+
static bool xsl_try_output_replaced_error_message(
223+
void *ctx,
224+
const char *msg,
225+
va_list args,
226+
const char *search,
227+
size_t search_len,
228+
const char *replace
229+
)
230+
{
231+
const char *msg_replace_location = strstr(msg, search);
232+
if (msg_replace_location != NULL) {
233+
php_libxml_ctx_error(ctx, "%.*s%s%s", (int) (msg_replace_location - msg), msg, replace, msg_replace_location + search_len);
234+
return true;
235+
}
236+
return false;
237+
}
238+
239+
/* Helper macro so the string length doesn't need to be passed separately.
240+
* Only allows literal strings for `search` and `replace`. */
241+
#define XSL_TRY_OUTPUT_REPLACED_ERROR_MESSAGE(ctx, msg, args, search, replace) \
242+
xsl_try_output_replaced_error_message(ctx, msg, args, "" search, sizeof("" search) - 1, "" replace)
243+
244+
/* We want to output PHP-tailored error messages for some libxslt error messages, such that
245+
* the errors refer to PHP properties instead of libxslt-specific fields. */
246+
static void xsl_libxslt_error_handler(void *ctx, const char *msg, ...)
247+
{
248+
va_list args;
249+
va_start(args, msg);
250+
251+
if (strcmp(msg, "%s") == 0) {
252+
/* Adjust error message to be more descriptive */
253+
const char *msg_arg = va_arg(args, const char *);
254+
bool output = XSL_TRY_OUTPUT_REPLACED_ERROR_MESSAGE(ctx, msg_arg, args, "xsltMaxDepth (--maxdepth)", "$maxTemplateDepth")
255+
|| XSL_TRY_OUTPUT_REPLACED_ERROR_MESSAGE(ctx, msg_arg, args, "maxTemplateVars (--maxvars)", "$maxTemplateVars");
256+
257+
if (!output) {
258+
php_libxml_ctx_error(ctx, "%s", msg_arg);
259+
}
260+
} else {
261+
php_libxml_error_handler_va(PHP_LIBXML_ERROR, ctx, msg, args);
262+
}
263+
264+
va_end(args);
265+
}
266+
125267
/* {{{ PHP_MINIT_FUNCTION */
126268
PHP_MINIT_FUNCTION(xsl)
127269
{
@@ -130,6 +272,10 @@ PHP_MINIT_FUNCTION(xsl)
130272
xsl_object_handlers.clone_obj = NULL;
131273
xsl_object_handlers.free_obj = xsl_objects_free_storage;
132274
xsl_object_handlers.get_gc = xsl_objects_get_gc;
275+
xsl_object_handlers.write_property = xsl_objects_write_property;
276+
xsl_object_handlers.get_property_ptr_ptr = xsl_objects_get_property_ptr_ptr;
277+
xsl_object_handlers.read_property = xsl_objects_read_property;
278+
xsl_object_handlers.unset_property = xsl_objects_unset_property;
133279

134280
xsl_xsltprocessor_class_entry = register_class_XSLTProcessor();
135281
xsl_xsltprocessor_class_entry->create_object = xsl_objects_new;
@@ -145,7 +291,7 @@ PHP_MINIT_FUNCTION(xsl)
145291
xsltRegisterExtModuleFunction ((const xmlChar *) "function",
146292
(const xmlChar *) "http://php.net/xsl",
147293
xsl_ext_function_object_php);
148-
xsltSetGenericErrorFunc(NULL, php_libxml_error_handler);
294+
xsltSetGenericErrorFunc(NULL, xsl_libxslt_error_handler);
149295

150296
register_php_xsl_symbols(module_number);
151297

ext/xsl/php_xsl.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ void xsl_objects_free_storage(zend_object *object);
7777
void xsl_ext_function_string_php(xmlXPathParserContextPtr ctxt, int nargs);
7878
void xsl_ext_function_object_php(xmlXPathParserContextPtr ctxt, int nargs);
7979

80+
zval *xsl_prop_max_template_depth(zend_object *object);
81+
zval *xsl_prop_max_template_vars(zend_object *object);
82+
8083
PHP_MINIT_FUNCTION(xsl);
8184
PHP_MSHUTDOWN_FUNCTION(xsl);
8285
PHP_RINIT_FUNCTION(xsl);

ext/xsl/php_xsl.stub.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class XSLTProcessor
7575

7676
public bool $cloneDocument = false;
7777

78+
public int $maxTemplateDepth;
79+
80+
public int $maxTemplateVars;
81+
7882
/**
7983
* @param DOMDocument|DOM\Document|SimpleXMLElement $stylesheet
8084
* @tentative-return-type

ext/xsl/php_xsl_arginfo.h

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/xsl/tests/bug71571_a.phpt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
Request #71571 (XSLT processor should provide option to change maxDepth) - variant A
3+
--EXTENSIONS--
4+
xsl
5+
--INI--
6+
error_reporting=E_ALL
7+
--FILE--
8+
<?php
9+
10+
$myxsl = <<<'EOF'
11+
<?xml version="1.0" encoding="UTF-8"?>
12+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
13+
<xsl:template match="/">
14+
<xsl:call-template name="recurse"/>
15+
</xsl:template>
16+
17+
<xsl:template name="recurse">
18+
<xsl:call-template name="recurse"/>
19+
</xsl:template>
20+
</xsl:stylesheet>
21+
EOF;
22+
23+
$xsl = new DOMDocument();
24+
$xsl->loadXML($myxsl);
25+
26+
$doc = new DOMDocument();
27+
28+
$proc = new XSLTProcessor;
29+
$proc->maxTemplateDepth = 2;
30+
$proc->importStyleSheet($xsl);
31+
$proc->transformToDoc($doc);
32+
33+
?>
34+
--EXPECTF--
35+
Warning: XSLTProcessor::transformToDoc(): runtime error: file %s line 8 element call-template in %s on line %d
36+
37+
Warning: XSLTProcessor::transformToDoc(): xsltApplySequenceConstructor: A potential infinite template recursion was detected.
38+
You can adjust $maxTemplateDepth in order to raise the maximum number of nested template calls and variables/params (currently set to 2). in %s on line %d
39+
40+
Warning: XSLTProcessor::transformToDoc(): Templates: in %s on line %d
41+
42+
Warning: XSLTProcessor::transformToDoc(): #0 name recurse in %s on line %d
43+
44+
Warning: XSLTProcessor::transformToDoc(): #1 name recurse in %s on line %d
45+
46+
Warning: XSLTProcessor::transformToDoc(): #2 name / in %s on line %d
47+
48+
Warning: XSLTProcessor::transformToDoc(): Variables: in %s on line %d

0 commit comments

Comments
 (0)