From c6bbe00e4c80d8faaa9f8dd3e57c2208f3edabbb Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 16 Jul 2026 08:21:22 -0400 Subject: [PATCH] ext/filter: apply options.default only when the filter fails php_zval_filter() decided whether to apply options.default by inspecting the filtered value for IS_FALSE, so a successful FILTER_VALIDATE_BOOL result of false was replaced by the default. FILTER_THROW_ON_FAILURE already keys off the handler's return code, so the two disagreed: filter_var("0", FILTER_VALIDATE_BOOL, FILTER_THROW_ON_FAILURE) does not throw, while the same input with a default returns the default. Have RETURN_VALIDATION_FAILED report FAILURE for every validation failure and key options.default off that, so both features share one notion of failure. --- UPGRADING | 7 ++++ ext/filter/filter.c | 7 ++-- ext/filter/filter_private.h | 18 ++++------ ext/filter/tests/filter_var_bool_default.phpt | 36 +++++++++++++++++++ 4 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 ext/filter/tests/filter_var_bool_default.phpt diff --git a/UPGRADING b/UPGRADING index 26e50b215f77..19332c7eef5c 100644 --- a/UPGRADING +++ b/UPGRADING @@ -38,6 +38,13 @@ PHP 8.6 UPGRADE NOTES the integer index is greater than INT_MAX instead of overflowing to a smaller index. +- Filter: + . The "default" option is now applied only when the filter actually fails, + matching FILTER_THROW_ON_FAILURE. Previously it was applied whenever the + filtered value was false, so a successful FILTER_VALIDATE_BOOL result of + false was replaced by the default. Use FILTER_NULL_ON_FAILURE if you need + to distinguish a valid false from a failure without a default. + - GD: . imagesetstyle(), imagefilter() and imagecrop() filter their array arguments types / values and raise a TypeError / ValueError accordingly. diff --git a/ext/filter/filter.c b/ext/filter/filter.c index b6e6332381e9..6d951aee24e3 100644 --- a/ext/filter/filter.c +++ b/ext/filter/filter.c @@ -240,6 +240,7 @@ static unsigned int php_sapi_filter_init(void) static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval *options, char* charset) /* {{{ */ { filter_list_entry filter_func; + zend_result result = FAILURE; filter_func = php_find_filter(filter); @@ -284,7 +285,7 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval copy_for_throwing = zend_string_copy(Z_STR_P(value)); } - zend_result result = filter_func.function(value, flags, options, charset); + result = filter_func.function(value, flags, options, charset); if (flags & FILTER_THROW_ON_FAILURE) { ZEND_ASSERT(copy_for_throwing != NULL); @@ -304,9 +305,7 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval } handle_default: - if (options && Z_TYPE_P(options) == IS_ARRAY && - ((flags & FILTER_NULL_ON_FAILURE && Z_TYPE_P(value) == IS_NULL) || - (!(flags & FILTER_NULL_ON_FAILURE) && Z_TYPE_P(value) == IS_FALSE))) { + if (options && Z_TYPE_P(options) == IS_ARRAY && result == FAILURE) { zval *tmp; if ((tmp = zend_hash_str_find(Z_ARRVAL_P(options), "default", sizeof("default") - 1)) != NULL) { ZVAL_COPY(value, tmp); diff --git a/ext/filter/filter_private.h b/ext/filter/filter_private.h index 14975e7911a4..f3cecb4d2fe0 100644 --- a/ext/filter/filter_private.h +++ b/ext/filter/filter_private.h @@ -95,23 +95,19 @@ /* When using FILTER_THROW_ON_FAILURE, we can't actually throw the error here * because we don't have access to the name of the filter. Returning FAILURE - * from the filter handler indicates that validation failed *and* an exception - * should thus be thrown. */ + * from the filter handler indicates that validation failed; php_zval_filter() + * decides whether that means throwing, applying options.default, or neither. */ #define RETURN_VALIDATION_FAILED \ if (EG(exception)) { \ return SUCCESS; \ - } else if (flags & FILTER_THROW_ON_FAILURE) { \ - zval_ptr_dtor(value); \ - ZVAL_NULL(value); \ - return FAILURE; \ - } else if (flags & FILTER_NULL_ON_FAILURE) { \ - zval_ptr_dtor(value); \ + } \ + zval_ptr_dtor(value); \ + if (flags & (FILTER_THROW_ON_FAILURE | FILTER_NULL_ON_FAILURE)) { \ ZVAL_NULL(value); \ } else { \ - zval_ptr_dtor(value); \ ZVAL_FALSE(value); \ - } \ - return SUCCESS; \ + } \ + return FAILURE; \ #define PHP_FILTER_TRIM_DEFAULT(p, len) PHP_FILTER_TRIM_DEFAULT_EX(p, len, 1); diff --git a/ext/filter/tests/filter_var_bool_default.phpt b/ext/filter/tests/filter_var_bool_default.phpt new file mode 100644 index 000000000000..bff3ecfa3d66 --- /dev/null +++ b/ext/filter/tests/filter_var_bool_default.phpt @@ -0,0 +1,36 @@ +--TEST-- +filter_var() FILTER_VALIDATE_BOOL keeps false when options.default is set +--EXTENSIONS-- +filter +--FILE-- + ['default' => 'OOPS']]; +foreach (['0', 'false', 'off', 'no', ''] as $v) { + var_dump($v, filter_var($v, FILTER_VALIDATE_BOOL, $opts)); +} +var_dump(filter_var('maybe', FILTER_VALIDATE_BOOL, $opts)); +var_dump(filter_var('true', FILTER_VALIDATE_BOOL, $opts)); +var_dump(filter_var('false', FILTER_VALIDATE_BOOL, [ + 'options' => ['default' => 'OOPS'], + 'flags' => FILTER_NULL_ON_FAILURE, +])); +var_dump(filter_var('maybe', FILTER_VALIDATE_BOOL, [ + 'options' => ['default' => 'OOPS'], + 'flags' => FILTER_NULL_ON_FAILURE, +])); +?> +--EXPECT-- +string(1) "0" +bool(false) +string(5) "false" +bool(false) +string(3) "off" +bool(false) +string(2) "no" +bool(false) +string(0) "" +bool(false) +string(4) "OOPS" +bool(true) +bool(false) +string(4) "OOPS"