Безрассудный validation php. Валидация и очистка данных средствами PHP

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

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

Не доверяйте данным пришедшим от пользователей, не под каким предлогом, не при каких обстоятельствах. Бывает такое, что лишний раз написать код проверяющий пришедшие данные просто лень или же мы надеяться, что существующих методов проверки достаточно, в результате чего делаем сами себе поблажки.

Немного отступления от темы:
Работа над проектами, разработка и программирование сайтов, скриптов, прочих систем занимает практически всё мое свободное время (по мимо рабочего), иными словами я занимаюсь этим делом максимально возможное количество часов в сутки. Время от времени возникает потребность в тестировании чего либо, ради забавы или просто любопытства. В итоге подобными тестируемыми лабораторными крысами становятся сайты сделанные на скорую руку, на самописных движках или на CMS древних версий. разумеется всё перечисленное страдает от криво написанного кода, нехватки котроля за данными и просто кишит различными багами. Собственно, в большинстве случаем за час моих экспериментов над такими сайтами мне удается найти несколько серьезных уязвимостей и большинство из них кроется в недостаточной валидации пришедших данных. В последнее время часто это встречается в скриптах обрабатывающих POST данные пришедших от JavaScript + Ajax.

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

как правило не малая часть таких скриптов оказывается на столько дырявой, что без особых усилий удается пробить брешь большего размера и залить свой шелл. разумеется исключительно с целью эксперимента и не более того (администрация таких сайтов всегда ставится в известность о имеющихся уязвимостях).

Думаю важность валидации всем понятна. На протяжении длительно времени я писал каждый раз один и тот же кусок кода, затем использовал собственные функции проверки данных, многие из которых были весьма примитивны и как правило разбросаны в разных частях (прииклюденных) файлов. В скором я начал знакомится с PHP фреймворками Zend, CI, Kohana, в каждом из был реализован свой класс для валидации данных, которые я заимствовал для своих проектов. В конце концов я решил заточить один из классов CI под свои нужды, но оказалось, что об этом уже позаботился автор одного из блогов по программированию. Далее делюсь его трудами, а именно модифицированной библиотекой CodeIgniter.

Разберем следующий код :

Просмотр кода PHP

require_once "validator.class.php" ; $validator = new Validator() ; $validator -> set_rules ("name" , "Ваше имя" , array ("required" => , "alpha" => ) ) ; $validator -> set_rules ("email" , "Ваш email" , array ("required" => "Поле %s обязательно для заполнения" , "valid_email" => ) ) ; if ($validator -> run () ) { echo "Валидация прошла успешно" ; } else { echo $validator -> get_string_errors () ; }

Как видно из примера, первой строкой мы подключаем файл класса validator.calss.php к нашем скрипту. Далее создаем экземпляр класса и сохраняем объект в переменную $validator .
Затем используя метод $validator->set_rules($field, $label, $rules) задаем поля для валидации.

Данный метод принимает 3 параметра:

  • $field - имя поля валидации (значение атрибута name в теге )
  • $label - название поля валидации, будет подставляться в сообщения об ошибках
  • $rules - массив правил валидации, у которого в качестве ключа используется правило валидации, а в качестве значения - сообщение об ошибке для этого правила
  • После того, как все поля для валидации установлены, запускаем валидатор используя метод $validator->run() . Если валидация прошла успешно, то данный метод вернет значение TRUE , иначе, если есть хоть какие-то ошибки, вернет FALSE .

    Для того чтобы получить сообщения об ошибках существует три метода:

  • get_string_errors() — возвращает все сообщения об ошибках в виде строки
  • get_array_errors() — возвращает все сообщения в виде массива, где в качестве ключа используется имя поля, а в качестве значения - описание ошибки для этого поля.
  • form_error($field) - возвращает сообщение об ошибке для поля, переданного в качестве параметра $field
  • По умолчанию сообщения об ошибках оборачиваются в тег . Для того чтобы задать свое оформление используйте метод set_error_delimiters($prefix, $suffix) . Например так:

    Теперь сообщения об ошибках будут оборачиваться в div с классом «error»

    Как видите все очень просто.

    Просмотр кода PHP

    $validator -> set_error_delimiters (" " , " " ) ;

    Для установки правил валидации Вы можете методу set_rules($fields) передать многомерный ассоциативный массив. Давайте посмотрим на пример:

    Просмотр кода PHP

    $rules = array ( array ( "field" => "name" , "label" => "Ваше имя" , "rules" => array ( "required" => "Поле %s обязательно для заполнения" , "alpha" => "Поле %s должно содержать только буквы" ) ) , array ( "field" => "email" , "label" => "Ваш email" , "rules" => array ( "required" => "Поле %s обязательно для заполнения" , "valid_email" => "Поле %s должно содержать правильный email-адрес" ) ) ) ; $validator -> set_rules ($rules ) ;

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

    Итак, какие же правила валидации поддерживает данный класс?

    Я вынес в этот класс наиболее распространенные правила валидации, с которыми сталкивается каждый. Вот полный список этих правил:

    required Возвращает FALSE если поле не заполнено
    integer Возвращает FALSE если значение не является целым числом
    float Возвращает FALSE если значение не числового вида
    valid_url Возвращает FALSE если значения не является корректным URL адресом
    valid_email Возвращает FALSE если значения не является корректным e-mail адресом
    valid_ip Возвращает FALSE если IP-адрес не является действительным
    matches Возвращает FALSE если элемент не соответствует значению другого элемента field
    alpha Возвращает FALSE если элемент содержит не только буквы
    valid_captcha Возвращает FALSE если значение в сессии field не равно значению поля формы
    valid_date Возвращает FALSE если элемент содержит не корректную дату

    Большинство этих правил используют фильтры , которые стали доступны в PHP 5.

    При желании вы всегда можете расширить набор правил для валидации, дописав нужные функции в класс Validator.

    Для того чтобы получить обработанное значение POST данных используется метод:

    Просмотр кода PHP

    Обычно данный метод вызываетя для очистки формы, при успешной обработке формы.

    Laravel поставляется с простой, удобной системой валидации (проверки входных данных на соответствие правилам) и получения сообщений об ошибках - классом Validation .

    Простейший пример валидации $validator = Validator::make(array("name" => "Дейл"), array("name" => "required|min:5"));

    Первый параметр, передаваемый методу make - данные для проверки. Второй параметр - правила, которые к ним должны быть применены.

    Использование массивов для указания правил

    Несколько правил могут быть разделены либо прямой чертой (|), либо быть отдельными элементами массива.

    $validator = Validator::make(array("name" => "Дейл"), array("name" => array("required", "min:5")));

    Проверка нескольких полей $validator = Validator::make(array("name" => "Дейл", "password" => "плохойпароль", "email" => "[email protected]"), array("name" => "required", "password" => "required|min:8", "email" => "required|email|unique"));

    Как только был создан экземпляр Validator , метод fails (или passes) может быть использован для проведения проверки.

    If ($validator->fails()) { // Переданные данные не прошли проверку }

    Если Validator нашёл ошибки, вы можете получить его сообщения таким образом:

    $messages = $validator->messages();

    Вы также можете получить массив правил, данные которые не прошли проверку, без самих сообщений:

    $failed = $validator->failed();

    Проверка файлов

    Класс Validator содержит несколько изначальных правил для проверки файлов, такие как size , mimes и другие. Для выполнения проверки над файлами просто передайте эти файлы вместе с другими данными.

    Хук после валидации

    Laravel после завершения валидации может запустить вашу функцию-замыкание, в которой вы можете, например, проверить что-то особенное или добавить какое-то своё сообщение об ошибке. Для этого служит метод after() :

    $validator = Validator::make(...); $validator->after(function($validator) { if ($this->somethingElseIsInvalid()) { $validator->errors()->add("field", "Something is wrong with this field!"); } }); if ($validator->fails()) { // }

    Вы можете добавить несколько after , если это нужно.

    Валидация в контроллерах

    Писать полный код валидации каждый раз, когда нужно провалидировать данные - это неудобно. Поэтому Laravel предоставляет несколько решений для упрощения этой процедуры.

    Базовый контроллер App\Http\Controllers\Controller включает в себя трейт ValidatesRequests , который уже содержит методы для валидации:

    /** * Сохранить пост в блоге. * * @param Request $request * @return Response */ public function store(Request $request) { $this->validate($request, [ "title" => "required|unique|max:255", "body" => "required", ]); // }

    Если валидация проходит, код продолжает выполняться. Если нет - бросается исключение Illuminate\Contracts\Validation\ValidationException . Если вы не поймаете это исключение, его поймает фреймворк, заполнит flash-переменные сообщениями об ошибках валидации и средиректит пользователя на предыдущую страницу с формой - сам!

    В случае AJAX-запроса редиректа не происходит, фреймворк отдает ответ с HTTP-кодом 422 и JSON с ошибками валидации.

    Код, приведенный выше, аналогичен вот этому::

    /** * Сохранить пост в блоге. * * @param Request $request * @return Response */ public function store(Request $request) { $v = Validator::make($request->all(), [ "title" => "required|unique|max:255", "body" => "required", ]); if ($v->fails()) { return redirect()->back()->withErrors($v->errors()); } // }

    Изменения формата ошибок

    Если вы хотите кастомизировать сообщения об ошибках валидации, которые сохраняются во флэш-переменных сессии при редиректе, перекройте метод formatValidationErrors в вашем контроллере:

    /** * {@inheritdoc} */ protected function formatValidationErrors(\Illuminate\Validation\Validator $validator) { return $validator->errors()->all(); } Валидация запросов

    Для реализации более сложных сценариев валидации вам могут быть удобны так называемые Form Requests. Это специальные классы HTTP-запроса, содержащие в себе логику валидации. Они обрабатывают запрос до того, как он поступит в контроллер.

    Чтобы создать класс запроса, используйте artisan-команду make:request:

    Php artisan make:request StoreBlogPostRequest

    Класс будет создан в папке app/Http/Requests . Добавьте необходимые правила валидации в его метод rules:

    /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ "title" => "required|unique|max:255", "body" => "required", ]; }

    Для того, чтобы фреймворк перехватил запрос перед контроллером, добавьте этот класс в аргументы необходимого метода контроллера:

    При грамотном использовании валидации запросов вы можете быть уверены, что в ваших контроллерах всегда находятся только отвалидированные входные данные!

    В случае неудачной валидации фреймворк заполняет флэш-переменные ошибками валидации и возврашает редирект на предыдущую страницу. В случае AJAX-запроса отдается ответ с кодом 422 и JSON с ошибками валидации.

    Контроль доступа

    Классы Form Request также содержат метод authorize . В этом методе вы можете проверять, разрешено ли пользователю совершать это действие, обновлять данный ресурс. Например, если пользователь пытается отредактировать комментарий к посту, является ли он его автором?

    /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { $commentId = $this->route("comment"); return Comment::where("id", $commentId) ->where("user_id", Auth::id())->exists(); }

    Обратите внимание на вызов метода route() выше. Этод метод дает вам доступ к параметрам в урле (в данном случае это {comment}), определенным в роуте:

    Route::post("comment/{comment}");

    Если метод authorize возвращает false, фреймворк формирует ответ с HTTP-кодом 403 и сразу же отсылает его. Метод контроллера не выполняется.

    /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; }

    Изменения формата ошибок во флэш-переменных

    Если вы хотите кастомизировать сообщения об ошибках валидации, которые сохраняются во флэш-переменных сессии при редиректе, переопределите метод formatValidationErrors в базовом классе запросов (App\Http\Requests\Request):

    /** * {@inheritdoc} */ protected function formatValidationErrors(\Illuminate\Validation\Validator $validator) { return $validator->errors()->all(); } Работа с сообщениями об ошибках

    После вызова метода messages объекта Validator вы получите объект MessageBag , который имеет набор полезных методов для доступа к сообщениям об ошибках.

    Получение первого сообщения для поля echo $messages->first("email"); Получение всех сообщений для одного поля foreach ($messages->get("email") as $message) { // } Получение всех сообщений для всех полей foreach ($messages->all() as $message) { // } Проверка на наличие сообщения для поля if ($messages->has("email")) { // } Получение ошибки в заданном формате echo $messages->first("email", "");

    Примечание: по умолчанию сообщения форматируются в вид, который подходит для Twitter Bootstrap.

    Получение всех сообщений в заданном формате foreach ($messages->all("
  • :message
  • ") as $message) { // } Ошибки и шаблоны

    Как только вы провели проверку, вам понадобится простой способ, чтобы передать ошибки в шаблон. Laravel позволяет удобно сделать это. Например, у нас есть такие роуты:

    Route::get("register", function() { return View::make("user.register"); }); Route::post("register", function() { $rules = array(...); $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { return redirect("register")->withErrors($validator); } });

    Заметьте, что когда проверки не пройдены, мы передаём объект Validator объекту переадресации Redirect с помощью метода withErrors . Этот метод сохранит сообщения об ошибках в одноразовых flash-переменных сессии, таким образом делая их доступными для следующего запроса.

    Однако, заметьте, мы не передаем в View::make("user.register"); переменные $errors в шаблон. Laravel сам проверяет данные сессии на наличие переменных и автоматически передает их шаблону, если они доступны. Таким образом, важно помнить, что переменная $errors будет доступна для всех ваших шаблонов всегда, при любом запросе. . Это позволяет вам считать, что переменная $errors всегда определена и может безопасно использоваться. Переменная $errors - экземпляр класса MessageBag .

    Таким образом, после переадресации вы можете прибегнуть к автоматически установленной в шаблоне переменной $errors:

    Именованные MessageBag

    Если у вас есть несколько форм на странице, то вы можете выбрать имя объекта MessageBag, в котором будут возвращаться тексты ошибок, чтобы вы могли их корректно отобразить для нужной формы.

    Return redirect("register")->withErrors($validator, "login");

    Получить текст ошибки из MessageBag с именем login:

    Правила проверки

    Ниже список всех доступных правил и их функции:

    accepted

    Поле должно быть в значении yes , on или 1 . Это полезно для проверки принятия правил и лицензий.

    active_url

    Поле должно быть корректным URL, доступным через функцию checkdnsrr .

    after:date

    Поле должно быть датой, более поздней, чем date

    alpha

    Поле должно содержать только алфавитные символы.

    alpha_dash

    Поле должно содержать только алфавитные символы, цифры, знаки подчёркивания (_) и дефисы (-).

    alpha_num

    Поле должно содержать только алфавитные символы и цифры.

    array

    Поле должно быть массивом.

    before:date

    Поле должно быть датой, более ранней, чем date . Строки приводятся к датам функцией strtotime .

    between:min ,max

    Поле должно иметь размер в диапазоне от min до max . Строки, числа и файлы трактуются аналогично правилу size .

    boolean

    Поле должно быть логическим (булевым). Разрешенные значения: true , false , 1 , 0 , "1" и "0" .

    confirmed

    Значение поля должно соответствовать значению поля с этим именем, плюс foo_confirmation . Например, если проверяется поле password , то на вход должно быть передано совпадающее по значению поле password_confirmation .

    date

    Поле должно быть правильной датой в соответствии с функцией strtotime .

    date_format:format

    Поле должно подходить под формат даты format в соответствии с функцией date_parse_from_format .

    different:field

    Значение проверяемого поля должно отличаться от значения поля field .

    email

    Поле должно быть корректным адресом e-mail.

    exists:table ,column

    Поле должно существовать в заданной таблице базе данных.

    Простое использование:

    "state" => "exists:states"

    Указание имени поля в таблице:

    "state" => "exists:states,abbreviation"

    Вы также можете указать больше условий, которые будут добавлены к запросу "WHERE":

    "email" => "exists:staff,email,account_id,1"

    image

    Загруженный файл должен быть изображением в формате jpeg, png, bmp, gif или svg.

    in:foo ,bar ,...

    Значение поля должно быть одним из перечисленных (foo , bar и т.д.).

    integer

    Поле должно иметь корректное целочисленное значение.

    ip

    Поле должно быть корректным IP-адресом.

    max:value

    Значение поля должно быть меньше или равно value

    mimes:foo ,bar ,...

    MIME-тип загруженного файла должен быть одним из перечисленных.

    Простое использование:

    "photo" => "mimes:jpeg,bmp,png"

    min:value

    Значение поля должно быть более value . Строки, числа и файлы трактуются аналогично правилу .

    not_in:foo ,bar ,...

    Значение поля не должно быть одним из перечисленных (foo , bar и т.д.).

    numeric

    Поле должно иметь корректное числовое или дробное значение.

    regex:pattern

    Поле должно соответствовать заданному регулярному выражению.

    Внимание: при использовании этого правила может быть нужно перечислять другие правила в виде элементов массива, особенно если выражение содержит символ вертикальной черты (|).

    required

    Проверяемое поле должно присутствовать и иметь непустое значение.

    required_if:field ,value ,...

    Проверяемое поле должно присутствовать и иметь непустое значение, если другое поле field присутствует и имеет любое из значений value .

    required_with:foo ,bar ,...

    Присутствует и имеет непустое значение хотя бы одно из перечисленных полей (foo , bar и т.д.).

    required_with_all:foo ,bar ,...

    Проверяемое поле должно присутствовать и иметь непустое значение, но только если присутствуют и имеют непустое значение все перечисленные поля (foo , bar и т.д.).

    required_without:foo ,bar ,...

    Проверяемое поле должно присутствовать и иметь непустое значение, но только если не присутствует или имеет пустое значение хотя бы одно из перечисленных полей (foo , bar и т.д.).

    required_without_all:foo ,bar ,...

    Проверяемое поле должно присутствовать и иметь непустое значение, но только если не присутствуют или имеют пустые значения все перечисленные поля (foo , bar и т.д.).

    same:field

    Поле должно иметь то же значение, что и поле field .

    size:value

    Поле должно иметь совпадающий с value размер. Для строк это обозначает длину, для чисел - число, для файлов - размер в килобайтах.

    timezone

    Поле должно содержать идентификатор часового пояса (таймзоны), один из перечисленных в php-функции timezone_identifiers_list

    unique:table ,column ,except ,idColumn

    Значение поля должно быть уникальным в заданной таблице базы данных. Если column не указано, то будет использовано имя поля.

    Простое использование "email" => "unique:users" Указание имени поля в таблице "email" => "unique:users,email_address" Игнорирование определённого ID "email" => "unique:users,email_address,10" Добавление дополнительных условий

    Вы также можете указать больше условий, которые будут добавлены к запросу "WHERE"":

    "email" => "unique:users,email_address,NULL,id,account_id,1"

    В правиле выше только строки с account_id равном 1 будут включены в проверку.

    url

    Поле должно быть корректным URL.

    Примечание: используется PHP-функция filter_var

    Условные правила

    Иногда вам нужно валидировать некое поле только тогда, когда оно присутствует во входных данных. Для этого добавьте правило sometimes:

    $v = Validator::make($data, array("email" => "sometimes|required|email",));

    В примере выше для поля email будет запущена валидация только когда $data["email"] существует.

    Сложные условные правила

    Иногда вам нужно, чтобы поле имело какое-либо значение только если другое поле имеет значеие, скажем, больше 100. Или вы можете требовать наличия двух полей, только когда также указано третье. Это легко достигается условными правилами. Сперва создайте объект Validator с набором статичных правил, которые никогда не изменяются:

    $v = Validator::make($data, array("email" => "required|email", "games" => "required|numeric",));

    Теперь предположим, что ваше приложение написано для коллекционеров игр. Если регистрируется коллекционер с более, чем 100 играми, то мы хотим их спросить, зачем им такое количество. Например, у них может быть магазин или может им просто нравится их собирать. Итак, для добавления такого условного правила мы используем метод Validator .

    $v->sometimes("reason", "required|max:500", function($input) { return $input->games >= 100; });

    Первый параметр этого метода - имя поля, которое мы проверяем. Второй параметр - правило, которое мы хотим добавить, если переданная функция-замыкание (третий параметр) вернёт true . Этот метод позволяет легко создавать сложные правила проверки ввода. Вы можете даже добавлять одни и те же условные правила для нескольких полей одновременно:

    $v->sometimes(array("reason", "cost"), "required", function($input) { return $input->games >= 100; });

    Примечание: Параметр $input , передаваемый замыканию - объект Illuminate\Support\Fluent и может использоваться для чтения проверяемого ввода и файлов.

    Собственные сообщения об ошибках

    Вы можете передать собственные сообщения об ошибках вместо используемых по умолчанию. Есть несколько способов это сделать.

    Передача своих сообщений в Validator $messages = array("required" => "Поле:attribute должно быть заполнено.",); $validator = Validator::make($input, $rules, $messages);

    Примечание: строка:attribute будет заменена на имя проверяемого поля. Вы также можете использовать и другие строки-переменные.

    Использование других переменных-строк $messages = array("same" => "Значения:attribute и:other должны совпадать.", "size" => "Поле:attribute должно быть равно exactly:size.", "between" => "Значение:attribute должно быть от:min и до:max.", "in" => "Поле:attribute должно иметь одно из следующих значений: :values",); Указание собственного сообщения для отдельного поля

    Иногда вам может потребоваться указать своё сообщение для отдельного поля.

    $messages = array("email.required" => "Нам нужно знать ваш e-mail адрес!",);

    Указание собственных сообщений в файле локализации

    Также можно определять сообщения валидации в файле локализации вместо того, чтобы передавать их в Validator напрямую. Для этого добавьте сообщения в массив custom файла локализации app/lang/xx/validation.php .

    "custom" => array("email" => array("required" => "Нам нужно знать ваш e-mail адрес!",),),

    Собственные правила проверки Регистрация собственного правила валидации

    Laravel изначально содержит множество полезных правил, однако вам может понадобиться создать собственные. Одним из способов зарегистрировать произвольное правило - через метод Validator::extend .

    Validator::extend("foo", function($attribute, $value, $parameters) { return $value == "foo"; });

    Примечание: имя правила должно быть в формате_с_подчёркиваниями.

    Переданная функция-замыкание получает три параметра: имя проверяемого поля $attribute , значение поля $value и массив параметров $parameters , переданных правилу.

    Вместо функции в метод extend можно передать ссылку на метод класса:

    Validator::extend("foo", "FooValidator@validate");

    Обратите внимание, что вам также понадобится определить сообщение об ошибке для нового правила. Вы можете сделать это либо передавая его в виде массива строк в Validator , либо вписав в файл локализации.

    Расширение класса Validator

    Вместо использования функций-замыканий для расширения набора доступных правил вы можете расширить сам класс Validator . Для этого создайте класс, который наследует Illuminate\Validation\Validator . Вы можете добавить новые методы проверок, начав их имя с validate .