-
Notifications
You must be signed in to change notification settings - Fork 28
Nested Validation Data #1993
Comments
@jpicornell, if you don't mind, I've simplified your example a bit: $data = [
'items' => [
['product' => ['id' => 1, 'name' => 'One']],
['product' => ['id' => 2, 'name' => 'Two']],
],
];
$rules = [
'items' => 'required', // this rule causes the problem
'items.*.product.id' => 'required',
];
$validator = Validator::make($data, $rules);
$validated = $validator->validate();
dd($validated); As you've already noted, this problem is caused by the I will continue to investigate, but, @driesvints, are you sure that it's not expected behavior? I understand that @jpicornell is trying to get only validated IDs, but we also can look at this from another angle. As we do have the rule for I feel that it's not an obvious bug, and possible PR can be rejected. |
I'm indeed not certain so I'm gonna de-label this as a bug for now. Will take a look at another time. |
I've checked Validator's tests, and there are no tests which cover a similar case. The validated data is composed here. It's just a simple So, probably, we have to analyze those keys in some way to solve this issue. I'm going to think about it more, but I'm still not sure that Taylor would accept such PR. |
@dmitry-ivanov No problem changing my example! The problem is that I control the request items to create Eloquent models. I see that in a way to build relations, if it's only the id, I associate the parent object, if I receive more fields, I associate and update. That's giving me problems and security concerns because a user can update fields I don't want them to in a given request. I don't know your previous thoughts about that, but I get it as if it is like not having nested validations. Required is only that this field should be present and not null, bot nothing more. According to the documentation: The main problem is that I cannot do that an array with nested validation is required if it's not through that validator, but if I remove the required validator the field can be empty, and I don't want that. Also I want to point that on first level validation ( items..name) is correctly validated, but nested of nested (items..product.id) is not. Maybe the solution to not break things is to create a "required_array" validation. I'll be here to give my thoughts and anything I could do to help, I'll do it. Thanks! |
@jpicornell Thank you for your detailed explanation! I feel your pain about that, and I've made a PR yesterday, which solves this problem. Would appreciate your and @driesvints feedback about PR. |
So, my first PR was closed. I've split it into two smaller PRs:
|
I don't think this is a bug, you have a |
Hi @themsaid , As I pointed, required only should be that |
You explicitly have a rule on the key Again it's opinionated and if it was ever merged I believe it should be in 5.8 and documented as a breaking change, and I'm almost sure the other camp will report the change as a bug :) |
@themsaid Let's see how it goes. I believe it should be in 5.8 too and documented as a breaking change, but I also see that's an error that should be addressed. If the field under validation is an array, an array should be considered "present and not empty" as if it is present on the request as an array and has more than 0 items. Returning all item's data is a possible security concern as it's happening to me, because a user could add any number of fields there. This can be a problem for NoSQL databases because I can add as many fields as I want without restriction. Maybe I'm wrong, but the validators is a great feature to transform and validate the user input into a data that fits your code, not vice versa. Thanks for your thoughts! |
Make an option to specify the strategy? I've never had a use case where I wanted to get the whole array instead of specific fields from it, so it's always been a pain to deal with so I'd like this to at least be an option 🙏 maybe a dummy rule to omit it from validated data? |
@donnysim Yes, that's the point I wanted to say. Getting the whole array only by specifying a "required" is giving "required" on arrays too much meaning that isn't on the documentation. I see this options by now: or we have another option to work the way we're saying and change the documentation of required to specify this behaviour (so we treat this as a feature) or we change the behaviour of required for arrays and make it like I said before: Arrays with required means that they have to be present and non-empty (more than one element). |
@jpicornell I don't think current logic should be changed how
We don't want to grab the whole
Mark rules with |
as of now neither: This is a gigantic security risk: where the whole purpose of the FormRequest class is to validate request and filter out other values. Now these other values are not filtered out! A solution to fix this in your local code, would be to extend the public function validated()
{
if ($this->invalid()) {
throw new ValidationException($this);
}
$results = [];
$missingValue = Str::random(10);
foreach ($this->getRules() as $key => $rules) {
if(in_array('array', $rules)){
continue; // skip array rules, expect to be sub rules.
}
$value = data_get($this->getData(), $key, $missingValue);
if ($value !== $missingValue) {
Arr::set($results, $key, $value);
}
}
return $results;
} warning: this solution renders usage of free arrays impossible! (these rules will simply be ignored and not part of the validated result set). |
Complete solution i'm using now: Note: also has a perf improvement by directly filling the validated result array, instead of separately filling this later (looping again over all rules) use Illuminate\Support\Arr;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\Factory;
class ValidationFactory extends Factory
{
/** {@inheritdoc} */
protected function resolve(array $data, array $rules, array $messages, array $customAttributes)
{
if (is_null($this->resolver)) {
return new Validator($this->translator, $data, $rules, $messages, $customAttributes);
}
return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
}
}
class Validator extends \Illuminate\Validation\Validator
{
protected $result = [];
/** {@inheritdoc} */
public function passes(): bool
{
$this->messages = new MessageBag();
$this->result = [];
$this->distinctValues = [];
foreach ($this->rules as $attribute => $rules) {
$attributeArrow = str_replace('\.', '->', $attribute);
$addToResult = true;
foreach ($rules as $rule) {
if ('array' === $rule) {
$addToResult = false;
}
$this->validateAttribute($attributeArrow, $rule);
if ($this->shouldStopValidating($attributeArrow)) {
break;
}
}
if ($addToResult) {
$this->setResultValueIfExists($attribute);
}
}
foreach ($this->after as $after) {
call_user_func($after);
}
return $this->messages->isEmpty();
}
/**
* Sets value of the validated request input in result.
*
* So result array will only contain null as value if this was also injected in the request.
* If property was omitted, this will also be omitted in result array. (Laravel base class uses same logic).
*
* Warning: using this in combination with the Eloquent\Model->fill() function,
* will lead to not updated attributes to null when they are omitted in the request.
*
* @param string $attribute key name of the parameter.
*/
protected function setResultValueIfExists(string $attribute): void
{
// potential perf improvement by using tryGet(array, attribute, outValue): bool method.
if(Arr::has($this->data, $attribute)){
Arr::set($this->result, $attribute, Arr::get($this->data, $attribute));
}
}
/** {@inheritdoc} */
public function validated(): array
{
return $this->result;
// return parent::validated();
}
}
class ValidationServiceProvider extends \Illuminate\Validation\ValidationServiceProvider
{
protected function registerValidationFactory()
{
$this->app->singleton('validator', function ($app) {
$validator = new ValidationFactory($app['translator'], $app);
// The validation presence verifier is responsible for determining the existence of
// values in a given data collection which is typically a relational database or
// other persistent data stores. It is used to check for "uniqueness" as well.
if (isset($app['db'], $app['validation.presence'])) {
$validator->setPresenceVerifier($app['validation.presence']);
}
return $validator;
});
}
} |
Maybe we need to use just this code $data = [
'items' => [
['product' => ['id' => 1, 'name' => 'One']],
['product' => ['id' => 2, 'name' => 'Two']],
],
];
$rules = [
// 'items' => 'required', ignore this
'items.*.product.id' => 'required', // we mark this is required, and it's must be required
];
$validator = Validator::make($data, $rules);
$validated = $validator->validate();
dd($validated); this code is not working for now if we try to validate $data = []; |
What happens if you try: $data = [
'items' => [
['product' => ['id' => 1, 'name' => 'One']],
['product' => ['id' => 2, 'name' => 'Two']],
],
];
$rules = [
'items.*' => 'required',
'items.*.product.id' => 'required',
];
$validator = Validator::make($data, $rules);
$validated = $validator->validate();
dd($validated); |
I ran into this issue myself with a complex form submission and nested array. I basically did something like this as a workaround:
Basically it seems that if you're using So something like the following will likely not give you want you expect with
The biggest issue as far as I can tell: you can't validate an array input (for example, to ensure it has a min/max number of elements) and also validate its nested data if you are relying on |
@trevorgehman but isn't that more of a feature request than an actual bug? |
The latest addition of
removes everything that matches Now that there's a I think in this case |
@driesvints Correct, I don't think this is a bug, just unexpected behavior. To your point, it make sense that I've thought about the simplest way to add this functionality, and personally think we could change this method: To something like: Or something more generic like: This way it wouldn't be a breaking change. |
@trevorgehman maybe yeah. I've transferred this issue to the Laravel ideas repo instead. |
I have ran into this right now, when I started to see some unexpected data in MySQL JSON column in my application. It didnt occur to me that requiring parent filed presence will put any unvalidated nested data into the "validated" data. This is definitely confusing and validation is useless for this purpouse. I hope there will be some way how to modify this behavior. |
I have run into this too. At least there should be a warning or something in the docs so people would not rely on validation for filtering nested array input data. |
There was a recent Twitter exchange about whether to use With that in mind, this issue is somewhat annoying because we can't rely on the However, I also saw that this might be a solution: laravel/framework#32452. |
I think this should be treated as a security vulnerability. Yes, there is fillable and guarded but that doesnt change the fact that the validator is misleading. At the 1st level it only includes the specified fields to give you a false sense of security and then at the second level comes the big gotcha. That solution is something but its inconsistent with how the 1st level params validate and then the docs should still have a big warning next to array validation in the docs. |
Description:
I try to validate an object field of an array on a JSON request and I want not to allow the user to introduce more fields on every object than the specified on that validator. When I call the validated() method on the request it returns all the fields.
It is the same as issue laravel/framework#23988 but for arrays on a nested field.
Steps To Reproduce:
You should have a validator with a field that's an array and this array includes an object field that has an id and some other field:
it should return:
but it returns the name field of the products too.
As a note, the bug comes when using the
'items'=> 'required|array'
validator, but I can't remove this validator because this array field should be present on the request always, even if it's empty. It has to be present on the request.The text was updated successfully, but these errors were encountered: