Skip to content

Commit d60aae4

Browse files
committed
streams: check class exists when attempting to register stream filter
And do a few other checks such as is the class even instantiable. It might make sense in the future to raise a deprecation if the class does not extend php_user_filter or create a new StreamFilter interface so that we know the various methods exist
1 parent 55c4eb2 commit d60aae4

4 files changed

Lines changed: 41 additions & 64 deletions

File tree

ext/standard/tests/filters/001.phpt

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,27 @@ stream_filter_register() and invalid arguments
33
--FILE--
44
<?php
55
try {
6-
stream_filter_register("", "");
7-
} catch (ValueError $exception) {
8-
echo $exception->getMessage() . "\n";
6+
stream_filter_register("", "stdClass");
7+
} catch (Throwable $e) {
8+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
99
}
1010

1111
try {
12-
stream_filter_register("test", "");
13-
} catch (ValueError $exception) {
14-
echo $exception->getMessage() . "\n";
12+
stream_filter_register("test", "Throwable");
13+
} catch (Throwable $e) {
14+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
1515
}
1616

1717
try {
18-
stream_filter_register("", "test");
19-
} catch (ValueError $exception) {
20-
echo $exception->getMessage() . "\n";
18+
var_dump(stream_filter_register("------", "nonexistentclass"));
19+
} catch (Throwable $e) {
20+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
2121
}
2222

23-
var_dump(stream_filter_register("------", "nonexistentclass"));
24-
2523
echo "Done\n";
2624
?>
2725
--EXPECT--
28-
stream_filter_register(): Argument #1 ($filter_name) must be a non-empty string
29-
stream_filter_register(): Argument #2 ($class) must be a non-empty string
30-
stream_filter_register(): Argument #1 ($filter_name) must be a non-empty string
31-
bool(true)
26+
ValueError: stream_filter_register(): Argument #1 ($filter_name) must be a non-empty string
27+
ValueError: stream_filter_register(): Argument #2 ($class) must be a concrete class
28+
TypeError: stream_filter_register(): Argument #2 ($class) must be a valid class name, nonexistentclass given
3229
Done

ext/standard/tests/filters/gh17037.phpt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
GH-17037 (UAF in user filter when adding existing filter name due to incorrect error handling)
33
--FILE--
44
<?php
5-
var_dump(stream_filter_register('string.toupper', 'filter_string_toupper'));
5+
try {
6+
var_dump(stream_filter_register('string.toupper', 'filter_string_toupper'));
7+
} catch (Throwable $e) {
8+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
9+
}
610
?>
711
--EXPECT--
8-
bool(false)
12+
TypeError: stream_filter_register(): Argument #2 ($class) must be a valid class name, filter_string_toupper given

ext/standard/tests/filters/stream_filter_register_non_existing_class.phpt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ stream_filter_register() with a class name that does not exist
33
--FILE--
44
<?php
55

6-
var_dump(stream_filter_register("not_existing_filter", "not_existing"));
6+
try {
7+
var_dump(stream_filter_register("not_existing_filter", "not_existing"));
8+
} catch (Throwable $e) {
9+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
10+
}
711

812
stream_filter_append(STDOUT, "not_existing_filter");
913

@@ -12,10 +16,8 @@ var_dump($out);
1216

1317
?>
1418
--EXPECTF--
15-
bool(true)
19+
TypeError: stream_filter_register(): Argument #2 ($class) must be a valid class name, not_existing given
1620

17-
Warning: stream_filter_append(): User-filter "not_existing_filter" requires class "not_existing", but that class is not defined in %s on line %d
18-
19-
Warning: stream_filter_append(): Unable to create or locate filter "not_existing_filter" in %s on line %d
21+
Warning: stream_filter_append(): Unable to locate filter "not_existing_filter" in %s on line %d
2022
Hello
2123
int(6)

ext/standard/user_filters.c

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,6 @@
2323
#define PHP_STREAM_BRIGADE_RES_NAME "userfilter.bucket brigade"
2424
#define PHP_STREAM_BUCKET_RES_NAME "userfilter.bucket"
2525

26-
struct php_user_filter_data {
27-
zend_class_entry *ce;
28-
/* variable length; this *must* be last in the structure */
29-
zend_string *classname;
30-
};
31-
3226
/* to provide context for calling into the next filter from user-space */
3327
static int le_bucket_brigade;
3428
static int le_bucket;
@@ -324,7 +318,6 @@ static const php_stream_filter_ops userfilter_ops = {
324318
static php_stream_filter *user_filter_factory_create(const char *filtername,
325319
zval *filterparams, bool persistent)
326320
{
327-
struct php_user_filter_data *fdat = NULL;
328321
php_stream_filter *filter;
329322
zval obj;
330323
zval retval;
@@ -343,8 +336,9 @@ static php_stream_filter *user_filter_factory_create(const char *filtername,
343336

344337
len = strlen(filtername);
345338

346-
/* determine the classname/class entry */
347-
if (NULL == (fdat = zend_hash_str_find_ptr(BG(user_filter_map), filtername, len))) {
339+
/* determine the class entry */
340+
/* const */ zend_class_entry *ce = zend_hash_str_find_ptr(BG(user_filter_map), filtername, len);
341+
if (UNEXPECTED(ce == NULL)) {
348342
const char *period;
349343

350344
/* Userspace Filters using ambiguous wildcards could cause problems.
@@ -362,7 +356,8 @@ static php_stream_filter *user_filter_factory_create(const char *filtername,
362356
ZEND_ASSERT(new_period[0] == '.');
363357
new_period[1] = '*';
364358
new_period[2] = '\0';
365-
if (NULL != (fdat = zend_hash_str_find_ptr(BG(user_filter_map), wildcard, strlen(wildcard)))) {
359+
ce = zend_hash_str_find_ptr(BG(user_filter_map), wildcard, strlen(wildcard));
360+
if (NULL != ce) {
366361
new_period = NULL;
367362
} else {
368363
*new_period = '\0';
@@ -371,21 +366,11 @@ static php_stream_filter *user_filter_factory_create(const char *filtername,
371366
}
372367
efree(wildcard);
373368
}
374-
ZEND_ASSERT(fdat);
375-
}
376-
377-
/* bind the classname to the actual class */
378-
if (fdat->ce == NULL) {
379-
if (NULL == (fdat->ce = zend_lookup_class(fdat->classname))) {
380-
php_error_docref(NULL, E_WARNING,
381-
"User-filter \"%s\" requires class \"%s\", but that class is not defined",
382-
filtername, ZSTR_VAL(fdat->classname));
383-
return NULL;
384-
}
369+
ZEND_ASSERT(ce);
385370
}
386371

387372
/* create the object */
388-
if (object_init_ex(&obj, fdat->ce) == FAILURE) {
373+
if (object_init_ex(&obj, ce) == FAILURE) {
389374
return NULL;
390375
}
391376

@@ -431,13 +416,6 @@ static const php_stream_filter_factory user_filter_factory = {
431416
user_filter_factory_create
432417
};
433418

434-
static void filter_item_dtor(zval *zv)
435-
{
436-
struct php_user_filter_data *fdat = Z_PTR_P(zv);
437-
zend_string_release_ex(fdat->classname, 0);
438-
efree(fdat);
439-
}
440-
441419
/* {{{ Return a bucket object from the brigade for operating on */
442420
PHP_FUNCTION(stream_bucket_make_writeable)
443421
{
@@ -600,41 +578,37 @@ PHP_FUNCTION(stream_get_filters)
600578
/* {{{ Registers a custom filter handler class */
601579
PHP_FUNCTION(stream_filter_register)
602580
{
603-
zend_string *filtername, *classname;
604-
struct php_user_filter_data *fdat;
581+
zend_string *filtername;
582+
zend_class_entry *ce;
605583

606584
ZEND_PARSE_PARAMETERS_START(2, 2)
607585
Z_PARAM_STR(filtername)
608-
Z_PARAM_STR(classname)
586+
Z_PARAM_CLASS(ce)
609587
ZEND_PARSE_PARAMETERS_END();
610588

611589
if (!ZSTR_LEN(filtername)) {
612590
zend_argument_value_error(1, "must be a non-empty string");
613591
RETURN_THROWS();
614592
}
615593

616-
if (!ZSTR_LEN(classname)) {
617-
zend_argument_value_error(2, "must be a non-empty string");
594+
/* TODO: Check class is a child of php_user_filter? */
595+
if (UNEXPECTED(ce->ce_flags & ZEND_ACC_UNINSTANTIABLE)) {
596+
zend_argument_value_error(2, "must be a concrete class");
618597
RETURN_THROWS();
619598
}
620599

621600
if (!BG(user_filter_map)) {
622601
BG(user_filter_map) = (HashTable*) emalloc(sizeof(HashTable));
623-
zend_hash_init(BG(user_filter_map), 8, NULL, (dtor_func_t) filter_item_dtor, 0);
602+
/* We don't need a destructor as we are only storing a CE which should be never modified */
603+
zend_hash_init(BG(user_filter_map), 8, NULL, NULL, 0);
624604
}
625605

626-
fdat = ecalloc(1, sizeof(struct php_user_filter_data));
627-
fdat->classname = zend_string_copy(classname);
628-
629-
if (zend_hash_add_ptr(BG(user_filter_map), filtername, fdat) != NULL) {
606+
if (zend_hash_add_ptr(BG(user_filter_map), filtername, ce) != NULL) {
630607
if (php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == SUCCESS) {
631608
RETURN_TRUE;
632609
}
633610

634611
zend_hash_del(BG(user_filter_map), filtername);
635-
} else {
636-
zend_string_release_ex(classname, 0);
637-
efree(fdat);
638612
}
639613

640614
RETURN_FALSE;

0 commit comments

Comments
 (0)