🎩 Книга «Денди-код» о том, как сделать код аккуратным и понятным

Защита от SQL-инъекций в Laravel: проверка параметров запроса

SQL-инъекции представляют серьезную угрозу для безопасности веб-приложений. В этой статье мы рассмотрим, как можно реализовать метод для проверки параметров запроса на наличие SQL-инъекций в Laravel, используя более универсальный и надежный подход.

Создание класса для проверки SQL-инъекций Сначала создадим класс AdvancedSqlInjectionChecker, который будет содержать метод для проверки строк на наличие SQL-инъекций.

class AdvancedSqlInjectionChecker
{
    /**
     * Проверяет, содержит ли строка потенциально опасные SQL-инъекции.
     *
     * @param string $input
     * @return bool
     */
    public static function hasSqlInjection(string $input): bool
    {
        // Набор шаблонов для обнаружения SQL-инъекций
        $patterns = [
            '/(?:\b(select|union|insert|update|delete|drop|alter|create|truncate)\b)/i', // Ключевые слова SQL
            '/(?:--|\#|\;)/', // Комментарии и точка с запятой
            '/(?:\b(and|or|xor|not)\b\s+[\w\s]+\s*(=|like|>|<|in|is|between)\s+[\w\s]+)/i', // Условные операторы
            '/(?:\b(?:exec|execute|sp_executesql|xp_cmdshell)\b)/i', // Команды выполнения
            '/(?:\b(select|union)[\s\S]+(from|join|into|load_file|information_schema|mysql)\b)/i', // Комбинированные шаблоны
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return true;
            }
        }

        // Дополнительная проверка на символы, которые часто используются в инъекциях
        $specialChars = ['\'', '"', ';', '\\', '--', '#'];

        foreach ($specialChars as $char) {
           if (str_contains($input, $char)) {
                return true;
            }
        }

        return false;
    }
}

Проверка параметров запроса в Laravel

Теперь интегрируем наш класс проверки в Laravel. Мы создадим Middleware, который будет проверять все параметры запроса на наличие SQL-инъекций.

Создадим новый Middleware:

php artisan make:middleware CheckForSqlInjection

Внутри созданного Middleware реализуем проверку:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use AdvancedSqlInjectionChecker;

class CheckForSqlInjection
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        $allInputs = array_merge($request->all(), $request->route()->parameters());

        foreach ($allInputs as $key => $value) {
            if (is_string($value) && AdvancedSqlInjectionChecker::hasSqlInjection($value)) {
                return response()->json(['error' => 'Potential SQL Injection detected in parameter: ' . $key], 400);
            }
        }

        return $next($request);
    }
}

Зарегистрируем Middleware в app/Http/Kernel.php:

protected $middleware = [
    // ...
    \App\Http\Middleware\CheckForSqlInjection::class,
];

Юнит-тестирование

Создадим юнит-тест для проверки работы класса AdvancedSqlInjectionChecker.

Создадим тестовый класс:

php artisan make:test AdvancedSqlInjectionCheckerTest

Реализуем тесты внутри созданного класса:

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Services\AdvancedSqlInjectionChecker;

class AdvancedSqlInjectionCheckerTest extends TestCase
{
    /**
     * @dataProvider sqlInjectionProvider
     */
    public function testHasSqlInjection($input, $expected)
    {
        $this->assertEquals($expected, AdvancedSqlInjectionChecker::hasSqlInjection($input));
    }

    public function sqlInjectionProvider()
    {
        return [
            ["SELECT * FROM users;", true],
            ["' OR 1=1 --", true],
            ["DROP TABLE users;", true],
            ["Safe string", false],
            ["12345", false],
        ];
    }
}

Заключение

Мы создали класс AdvancedSqlInjectionChecker, который проверяет строки на наличие SQL-инъекций с помощью регулярных выражений и анализа специальных символов. Затем мы интегрировали эту проверку в Laravel с помощью Middleware, который анализирует все параметры входящих запросов.

Этот метод позволяет обнаруживать потенциальные SQL-инъекции на раннем этапе и предотвращать выполнение опасных запросов. Однако, для полной защиты рекомендуется использовать подготовленные выражения и экранирование данных.

Пример использования Допустим, у нас есть контроллер, который принимает параметры запроса:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    public function getUser(Request $request)
    {
        $name = $request->input('name');
        $email = $request->input('email');

        // Допустим, здесь мы используем Eloquent для поиска пользователя
        $user = \App\Models\User::where('name', $name)->where('email', $email)->first();

        return response()->json($user);
    }
}

С включенным Middleware CheckForSqlInjection, каждый параметр запроса будет проверяться на наличие SQL-инъекций перед выполнением основного кода контроллера. Это добавляет дополнительный уровень безопасности вашему приложению.

2

Паттерн Конвейер (Pipeline Pattern) в Laravel

Пайплайны предоставляют элегантное решение для последовательного применения множества фильтров к запросу Eloquent. В Laravel пайплайны позволяют инкапсулировать логику фильтрации в отдельные классы и затем последовательно применять их к запросу. Это повышает читаемость, тестируемость и расширяемость кода.

Давайте рассмотрим, как можно использовать пайплайны для реализации фильтров в Eloquent:

Шаг 1: Создание фильтров

Каждый фильтр будет отдельным классом, реализующим интерфейс FilterInterface.

<?php

namespace App\Pipelines\Filters;

use Illuminate\Database\Eloquent\Builder;

interface FilterInterface
{
    public static function apply(Builder $builder, $value): Builder;
}

Примеры фильтров:

Фильтр по названию:

<?php

namespace App\Pipelines\Filters;

use Illuminate\Database\Eloquent\Builder;

class NameFilter implements FilterInterface
{
    public static function apply(Builder $builder, $value): Builder
    {
        return $builder->where('name', 'like', '%' . $value . '%');
    }
}

Фильтр по категории:

<?php

namespace App\Pipelines\Filters;

use Illuminate\Database\Eloquent\Builder;

class CategoryFilter implements FilterInterface
{
    public static function apply(Builder $builder, $value): Builder
    {
        return $builder->where('category', $value);
    }
}

Фильтр по цене:

<?php

namespace App\Pipelines\Filters;

use Illuminate\Database\Eloquent\Builder;

class PriceFilter implements FilterInterface
{
    public static function apply(Builder $builder, $value): Builder
    {
        return $builder->whereBetween('price', [$value['min'], $value['max']]);
    }
}

Шаг 2: Создание класса пайплайна

Создайте класс ProductPipeline, который будет последовательно применять фильтры.

<?php

namespace App\Pipelines;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Pipeline;

class ProductPipeline
{
    protected array $filters = [];

    public function __construct(array $filters)
    {
        $this->filters = $filters;
    }

    public function apply(Builder $builder): Builder
    {
        return app(Pipeline::class)
            ->send($builder)
            ->through($this->filters)
            ->thenReturn();
    }
}

Шаг 3: Применение пайплайнов в контроллере

Используйте пайплайн в контроллере для фильтрации данных.

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use App\Pipelines\Filters\NameFilter;
use App\Pipelines\Filters\CategoryFilter;
use App\Pipelines\Filters\PriceFilter;
use App\Pipelines\ProductPipeline;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

/**
 * Class ProductController
 */
class ProductController extends Controller
{
    /**
     * Получение списка продуктов с фильтрацией
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function index(Request $request): JsonResponse
    {
        $pipeline = new ProductPipeline([
            NameFilter::class,
            CategoryFilter::class,
            PriceFilter::class,
        ]);

        $products = $pipeline->apply(Product::query())->get();

        return response()->json($products);
    }
}

Шаг 4: Валидация входящих данных

Создайте FormRequest для валидации входящих данных.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

/**
 * Class ProductFilterRequest
 */
class ProductFilterRequest extends FormRequest
{
    /**
     * Правила валидации
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'name'     => 'nullable|string|max:255',
            'category' => 'nullable|string|max:255',
            'price'    => 'nullable|array',
            'price.min'=> 'nullable|numeric|min:0',
            'price.max'=> 'nullable|numeric|min:0',
        ];
    }
}

Заключение

Использование пайплайнов для фильтрации данных в Eloquent позволяет легко и элегантно управлять сложными запросами. Это улучшает читаемость кода и упрощает его поддержку, предоставляя более гибкий и модульный способ обработки запросов.

1