abkrim's avatar
Level 13

Validate in params exists. Injection Prevention or Input Validation

Hi

At first, I was thinking about validating parameters issued in an API call, to emit the error in the response. Still, I also thought avoiding malicious injections was a good measure. (Injection Prevention)

Let's say for example that John Doe calls our API and sends a fake parameter with the content ../../../../../../etc/passwd

It's a very crude example but it's about that.

 public function rules(): array
    {
        $rules = [
            'filter.id' => 'integer|filled',
            'filter.button_type' => 'string|filled',
            'filter.name' => 'string|filled',
            'filter.endpoint' => 'string|filled',
            'filter.parameters' => 'string|filled',
            'filter.config_enum_access_level' => 'integer|filled',
            'filter.enabled' => 'integer|filled',
            'filter.manufacturer_id' => 'filled',
            'filter.is_multiple' => 'integer|filled',
        ];

        $rules['*'] = function ($attribute, $value, $fail) {
            $allowedKeys = [
                'filter.id', 'filter.button_type', 'filter.name', 'filter.endpoint', 'filter.parameters',
                'filter.config_enum_access_level', 'filter.enabled', 'filter.manufacturer_id',
                'filter.is_multiple', 'filter.parameters',
            ];

            if (! in_array($attribute, $allowedKeys)) {
                $fail("The parameter '{$attribute}' is not allowed.");
            }
        };

        return $rules;
    }

    public function validationData(): array
    {
        return $this->all();
    }

I have created this, but I don't know if I am reinventing the wheel and there is something already realized.

I must also say that due to contractual obligations with the development of that company, I am still working with Laravel 10.

It's right? Is there another way?

Test

Added after some responses.

it('user passes invalid params to app button filter request fails', function () {
    $user = loginAsAdmin();

    $request = new AppButtonFilterRequest();

    $request->setContainer($this->app)->setUserResolver(fn () => $user)
        ->initialize(['fake_param' => 'fake_value']);

    $request->authorize();

    $validator = Validator::make($request->all(), $request->rules());

    $request->validationData();

    expect($validator->fails())->toBeTrue()
        ->and($validator->errors()->first('fake_param'))->toBe('The parameter \'fake_param\' is not allowed.');
});
0 likes
9 replies
LaryAI's avatar
Level 58

To address the problem of validating parameters in an API call and preventing malicious injections, you can leverage Laravel's built-in validation mechanisms. Your approach is on the right track, but it can be refined for better clarity and security.

Here's a refined solution:

  1. Define the validation rules: You can define the validation rules for each parameter as you have done.
  2. Custom validation for allowed keys: Instead of using a custom closure for each attribute, you can use Laravel's in rule to ensure that only allowed keys are present.

Here's an improved version of your code:

public function rules(): array
{
    // Define the allowed keys
    $allowedKeys = [
        'filter.id', 'filter.button_type', 'filter.name', 'filter.endpoint', 'filter.parameters',
        'filter.config_enum_access_level', 'filter.enabled', 'filter.manufacturer_id',
        'filter.is_multiple',
    ];

    // Define the validation rules
    $rules = [
        'filter.id' => 'integer|filled',
        'filter.button_type' => 'string|filled',
        'filter.name' => 'string|filled',
        'filter.endpoint' => 'string|filled',
        'filter.parameters' => 'string|filled',
        'filter.config_enum_access_level' => 'integer|filled',
        'filter.enabled' => 'integer|filled',
        'filter.manufacturer_id' => 'filled',
        'filter.is_multiple' => 'integer|filled',
    ];

    // Add a rule to ensure only allowed keys are present
    foreach ($allowedKeys as $key) {
        $rules[$key] = $rules[$key] . '|in:' . implode(',', $allowedKeys);
    }

    return $rules;
}

public function validationData(): array
{
    return $this->all();
}

Explanation:

  1. Allowed Keys: The $allowedKeys array contains all the keys that are allowed in the request.
  2. Validation Rules: The $rules array defines the validation rules for each parameter.
  3. Ensuring Only Allowed Keys: The in rule is used to ensure that only the keys specified in $allowedKeys are present in the request. This prevents any additional or malicious parameters from being processed.

Additional Security Measures:

  • Sanitize Input: Always sanitize input data to remove any potentially harmful content.
  • Use Prepared Statements: If you are using the input data in database queries, always use prepared statements to prevent SQL injection.
  • Escape Output: When displaying user input, always escape the output to prevent XSS attacks.

By following these practices, you can ensure that your API is secure and robust against malicious injections and invalid input.

abkrim's avatar
Level 13

@LaryAI This code does not pass validation for security.

it('user passes invalid params to app button filter request fails', function () {
    $user = loginAsAdmin();

    $request = new AppButtonFilterRequest();

    $request->setContainer($this->app)->setUserResolver(fn () => $user)
        ->initialize(['fake_param' => 'fake_value']);

    $request->authorize();

    $validator = Validator::make($request->all(), $request->rules());

    $request->validationData();

    expect($validator->fails())->toBeTrue()
        ->and($validator->errors()->first('fake_param'))->toBe('The parameter \'fake_param\' is not allowed.');
});
martinbean's avatar

@abkrim You can specify keys with the array rule. Laravel will then only validate those keys, and reject any others:

return [
    'filter' => ['required', 'array:id,name'],
];

You will then only get filter.id and filter.name back if you use $request->validated() to retrieve request input.

From https://laravel.com/docs/11.x/validation#rule-array:

When additional values are provided to the array rule, each key in the input array must be present within the list of values provided to the rule.

abkrim's avatar
Level 13

@martinbean I disagree.

required, if present, requires that the field be in the parameters.

What the post is about is "not accepting" parameters that do not exist in the rules.

Maybe I didn't express myself well, but I think so.

It is about stopping any parameter that does NOT exist in the validation rules.

newbie360's avatar

@abkrim If i understand correctly the question, that's no permission on web server layer

abkrim's avatar
Level 13

Well, we think about things that don't seem to have anything to do with my difficulty in the language.

Of course there is a layer of web server security, but that's not the point.

The security layer is from my server, not from the application. Then if the application travels to another server and does not have said layer, it is another matter.

The question is:

How to protect the app in an API, in such a way that if a user sends a request to the API, and enters a non-existent parameter, they receive an error message from the validation system (request form) indicating that the parameter is not allowed.

The code at the beginning is clear, concise and explanatory, Now I have modified it to show the test code, which makes it more explanatory.

Along with the test, the scope of the question and the process.

thank you.

newbie360's avatar

@abkrim

What the post is about is "not accepting" parameters that do not exist in the rules.

dd(
    $request->all(), // danger
    $request->validated() // safe
);
abkrim's avatar
Level 13

@newbie360 Please argue why.

In my code

public function validationData(): array
{
    return $this->all();
}

Only has a validation, for does the parameter exist or not? If it does not exist, then the user passed an invalid parameter.

What's so dangerous about this?

Best regards

newbie360's avatar

@abkrim where the validationData() come from, read the doc https://laravel.com/docs/11.x/validation#form-request-validation

scroll down a bit, you can see an example

public function store(StorePostRequest $request): RedirectResponse
{
    // The incoming request is valid...
 
    // Retrieve the validated input data...
    $validated = $request->validated();
 
    // Retrieve a portion of the validated input data...
    $validated = $request->safe()->only(['name', 'email']);
    $validated = $request->safe()->except(['name', 'email']);
 
    // Store the blog post...
 
    return redirect('/posts');
}

$request->validated() only return an array contain the title data

public function rules(): array
{
    return [
        'title' => 'required|string|max:255',
    ];
}

so your problem isn't how to prevent the client send fake data, all you need is get the safe data based on the rules, thats why we need validation

in the above example, try to send a request with some fake params, you can see the difference

public function store(StorePostRequest $request): RedirectResponse
{
    dd(
        $request->all(), // this contain all fake params data
        $request->validated(), // this only contain title data
    );
}

and also read https://laravel.com/docs/11.x/eloquent-resources

Please or to participate in this conversation.