Skip to content

[RFC] FILTER_THROW_ON_FAILURE #18896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ext/filter/callback_filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

#include "php_filter.h"

void php_filter_callback(PHP_INPUT_FILTER_PARAM_DECL)
zend_result php_filter_callback(PHP_INPUT_FILTER_PARAM_DECL)
{
zval retval;
int status;
Expand All @@ -25,7 +25,7 @@ void php_filter_callback(PHP_INPUT_FILTER_PARAM_DECL)
zend_type_error("%s(): Option must be a valid callback", get_active_function_name());
zval_ptr_dtor(value);
ZVAL_NULL(value);
return;
return SUCCESS;
}

status = call_user_function(NULL, NULL, option_array, &retval, 1, value);
Expand All @@ -37,4 +37,5 @@ void php_filter_callback(PHP_INPUT_FILTER_PARAM_DECL)
zval_ptr_dtor(value);
ZVAL_NULL(value);
}
return SUCCESS;
}
118 changes: 109 additions & 9 deletions ext/filter/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ ZEND_DECLARE_MODULE_GLOBALS(filter)

#include "filter_private.h"
#include "filter_arginfo.h"
#include "zend_exceptions.h"

typedef struct filter_list_entry {
const char *name;
int id;
void (*function)(PHP_INPUT_FILTER_PARAM_DECL);
zend_result (*function)(PHP_INPUT_FILTER_PARAM_DECL);
} filter_list_entry;

/* {{{ filter_list */
Expand Down Expand Up @@ -76,6 +77,9 @@ static const filter_list_entry filter_list[] = {
static unsigned int php_sapi_filter(int arg, const char *var, char **val, size_t val_len, size_t *new_val_len);
static unsigned int php_sapi_filter_init(void);

zend_class_entry *php_filter_exception_ce;
zend_class_entry *php_filter_failed_exception_ce;

/* {{{ filter_module_entry */
zend_module_entry filter_module_entry = {
STANDARD_MODULE_HEADER,
Expand Down Expand Up @@ -159,6 +163,9 @@ PHP_MINIT_FUNCTION(filter)

sapi_register_input_filter(php_sapi_filter, php_sapi_filter_init);

php_filter_exception_ce = register_class_Filter_FilterException(zend_ce_exception);
php_filter_failed_exception_ce = register_class_Filter_FilterFailedException(php_filter_exception_ce);

return SUCCESS;
}
/* }}} */
Expand Down Expand Up @@ -250,6 +257,16 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval
ce = Z_OBJCE_P(value);
if (!ce->__tostring) {
zval_ptr_dtor(value);
if (flags & FILTER_THROW_ON_FAILURE) {
zend_throw_exception_ex(
php_filter_failed_exception_ce,
0,
"filter validation failed: object of type %s has no __toString() method",
ZSTR_VAL(ce->name)
);
ZVAL_NULL(value);
return;
}
/* #67167: doesn't return null on failure for objects */
if (flags & FILTER_NULL_ON_FAILURE) {
ZVAL_NULL(value);
Expand All @@ -263,7 +280,29 @@ static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval
/* Here be strings */
convert_to_string(value);

filter_func.function(value, flags, options, charset);
zend_string *copy_for_throwing = NULL;
if (flags & FILTER_THROW_ON_FAILURE) {
copy_for_throwing = zend_string_copy(Z_STR_P(value));

This comment was marked as resolved.

This comment was marked as resolved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe that the filter function can change things, specifically around the use of sanitizing flags - we need to identify the original string before it changes, hence the use of a copy

}

zend_result result = filter_func.function(value, flags, options, charset);

if (flags & FILTER_THROW_ON_FAILURE) {
ZEND_ASSERT(copy_for_throwing != NULL);
if (result == FAILURE) {
zend_throw_exception_ex(
php_filter_failed_exception_ce,
0,
"filter validation failed: filter %s not satisfied by %s",
filter_func.name,
ZSTR_VAL(copy_for_throwing)
);
zend_string_delref(copy_for_throwing);
return;
}
zend_string_delref(copy_for_throwing);
copy_for_throwing = NULL;
}

handle_default:
if (options && Z_TYPE_P(options) == IS_ARRAY &&
Expand Down Expand Up @@ -449,7 +488,8 @@ PHP_FUNCTION(filter_has_var)

static void php_filter_call(
zval *filtered, zend_long filter, HashTable *filter_args_ht, zend_long filter_args_long,
zend_long filter_flags
zend_long filter_flags,
uint32_t options_arg_num
) /* {{{ */ {
zval *options = NULL;
char *charset = NULL;
Expand Down Expand Up @@ -491,10 +531,28 @@ static void php_filter_call(
}
}

/* Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE */
if ((filter_flags & FILTER_NULL_ON_FAILURE) && (filter_flags & FILTER_THROW_ON_FAILURE)) {
zval_ptr_dtor(filtered);
ZVAL_NULL(filtered);
zend_argument_value_error(
options_arg_num,
"cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE"
);
return;
}

if (Z_TYPE_P(filtered) == IS_ARRAY) {
if (filter_flags & FILTER_REQUIRE_SCALAR) {
zval_ptr_dtor(filtered);
if (filter_flags & FILTER_NULL_ON_FAILURE) {
if (filter_flags & FILTER_THROW_ON_FAILURE) {
ZVAL_NULL(filtered);
zend_throw_exception(
php_filter_failed_exception_ce,
"filter validation failed: not a scalar value (got an array)",
0
);
} else if (filter_flags & FILTER_NULL_ON_FAILURE) {
ZVAL_NULL(filtered);
} else {
ZVAL_FALSE(filtered);
Expand All @@ -505,6 +563,17 @@ static void php_filter_call(
return;
}
if (filter_flags & FILTER_REQUIRE_ARRAY) {
if (filter_flags & FILTER_THROW_ON_FAILURE) {
zend_throw_exception_ex(
php_filter_failed_exception_ce,
0,
"filter validation failed: not an array (got %s)",
zend_zval_value_name(filtered)
);
zval_ptr_dtor(filtered);
ZVAL_NULL(filtered);
return;
}
zval_ptr_dtor(filtered);
if (filter_flags & FILTER_NULL_ON_FAILURE) {
ZVAL_NULL(filtered);
Expand All @@ -515,6 +584,10 @@ static void php_filter_call(
}

php_zval_filter(filtered, filter, filter_flags, options, charset);
// Don't wrap in an array if we are throwing an exception
if (EG(exception)) {
return;
}
if (filter_flags & FILTER_FORCE_ARRAY) {
zval tmp;
ZVAL_COPY_VALUE(&tmp, filtered);
Expand All @@ -529,7 +602,7 @@ static void php_filter_array_handler(zval *input, HashTable *op_ht, zend_long op
) /* {{{ */ {
if (!op_ht) {
ZVAL_DUP(return_value, input);
php_filter_call(return_value, -1, NULL, op_long, FILTER_REQUIRE_ARRAY);
php_filter_call(return_value, -1, NULL, op_long, FILTER_REQUIRE_ARRAY, 2);
} else {
array_init(return_value);
zend_string *arg_key;
Expand All @@ -556,8 +629,12 @@ static void php_filter_array_handler(zval *input, HashTable *op_ht, zend_long op
php_filter_call(&nval, -1,
Z_TYPE_P(arg_elm) == IS_ARRAY ? Z_ARRVAL_P(arg_elm) : NULL,
Z_TYPE_P(arg_elm) == IS_ARRAY ? 0 : zval_get_long(arg_elm),
FILTER_REQUIRE_SCALAR
FILTER_REQUIRE_SCALAR,
2
);
if (EG(exception)) {
RETURN_THROWS();
}
zend_hash_update(Z_ARRVAL_P(return_value), arg_key, &nval);
}
} ZEND_HASH_FOREACH_END();
Expand Down Expand Up @@ -597,11 +674,34 @@ PHP_FUNCTION(filter_input)
if (!filter_args_ht) {
filter_flags = filter_args_long;
} else {
zval *option, *opt, *def;
zval *option;
if ((option = zend_hash_str_find(filter_args_ht, "flags", sizeof("flags") - 1)) != NULL) {
filter_flags = zval_get_long(option);
}
}

/* Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE */
if ((filter_flags & FILTER_NULL_ON_FAILURE) && (filter_flags & FILTER_THROW_ON_FAILURE)) {
zend_argument_value_error(
4,
"cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE"
);
RETURN_THROWS();
}

if (filter_flags & FILTER_THROW_ON_FAILURE) {
zend_throw_exception(
php_filter_failed_exception_ce,
"input value not found",
0
);
RETURN_THROWS();
}

/* FILTER_THROW_ON_FAILURE overrides defaults, needs to be checked
* before the default is used. */
if (filter_args_ht) {
zval *opt, *def;
if ((opt = zend_hash_str_find_deref(filter_args_ht, "options", sizeof("options") - 1)) != NULL &&
Z_TYPE_P(opt) == IS_ARRAY &&
(def = zend_hash_str_find_deref(Z_ARRVAL_P(opt), "default", sizeof("default") - 1)) != NULL
Expand All @@ -625,7 +725,7 @@ PHP_FUNCTION(filter_input)

ZVAL_DUP(return_value, tmp);

php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR);
php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR, 4);
}
/* }}} */

Expand All @@ -651,7 +751,7 @@ PHP_FUNCTION(filter_var)

ZVAL_DUP(return_value, data);

php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR);
php_filter_call(return_value, filter, filter_args_ht, filter_args_long, FILTER_REQUIRE_SCALAR, 3);
}
/* }}} */

Expand Down
16 changes: 16 additions & 0 deletions ext/filter/filter.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/** @generate-class-entries */

namespace {
/**
* @var int
* @cvalue PARSE_POST
Expand Down Expand Up @@ -54,6 +55,11 @@
* @cvalue FILTER_NULL_ON_FAILURE
*/
const FILTER_NULL_ON_FAILURE = UNKNOWN;
/**
* @var int
* @cvalue FILTER_THROW_ON_FAILURE
*/
const FILTER_THROW_ON_FAILURE = UNKNOWN;

/**
* @var int
Expand Down Expand Up @@ -313,3 +319,13 @@ function filter_var_array(array $array, array|int $options = FILTER_DEFAULT, boo
function filter_list(): array {}

function filter_id(string $name): int|false {}

}

namespace Filter {

class FilterException extends \Exception {}

class FilterFailedException extends FilterException {}

}
23 changes: 22 additions & 1 deletion ext/filter/filter_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions ext/filter/filter_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

#define FILTER_FORCE_ARRAY 0x4000000
#define FILTER_NULL_ON_FAILURE 0x8000000
#define FILTER_THROW_ON_FAILURE 0x10000000

#define FILTER_FLAG_ALLOW_OCTAL 0x0001
#define FILTER_FLAG_ALLOW_HEX 0x0002
Expand All @@ -50,7 +51,7 @@
#define FILTER_FLAG_IPV6 0x00200000
#define FILTER_FLAG_NO_RES_RANGE 0x00400000
#define FILTER_FLAG_NO_PRIV_RANGE 0x00800000
#define FILTER_FLAG_GLOBAL_RANGE 0x10000000
#define FILTER_FLAG_GLOBAL_RANGE 0x20000000

#define FILTER_FLAG_HOSTNAME 0x100000

Expand Down Expand Up @@ -93,17 +94,26 @@
|| (id >= FILTER_VALIDATE_ALL && id <= FILTER_VALIDATE_LAST) \
|| id == FILTER_CALLBACK)


/* 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. */
#define RETURN_VALIDATION_FAILED \
if (EG(exception)) { \
return; \
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_NULL(value); \
} else { \
zval_ptr_dtor(value); \
ZVAL_FALSE(value); \
} \
return; \
return SUCCESS; \

#define PHP_FILTER_TRIM_DEFAULT(p, len) PHP_FILTER_TRIM_DEFAULT_EX(p, len, 1);

Expand Down
Loading
Loading