• Время чтения ~2 мин
  • 25.06.2022

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

Однако, как разработчик, мы абсолютно не можем доверять нашим пользователям. Всегда!

Когда мы говорим о доверии пользователям, это означает, что мы не можем доверять их информации. Мы не можем доверять им в предоставлении правильной информации, использовании правильных форматов или даже выполнении только ожидаемых действий.Проблема в том, что пользователи — сложные и разочаровывающие существа. У некоторых пользователей есть злонамеренные намерения, и они будут искать способы сломать или скомпрометировать ваши приложения. Другие могут думать иначе, чем вы, и будут делать то, чего вы не ожидаете, — натыкаться на ошибки и получать доступ к данным, к которым у них не должно быть доступа (моя жена — эксперт в этом!).А другие просто теряются и сбиты с толку и будут пытаться вернуться к тому, с чего начали.

Поэтому наша задача как разработчиков состоит в том, чтобы защитить наши приложения от пользователей. Мы должны параноидально относиться к входным данным, которые мы получаем, и иметь несколько средств защиты, чтобы гарантировать, что независимо от того, что делает пользователь, наше приложение будет безопасным. О, и каким-то образом убедите наших пользователей, что мы им не доверяем, поэтому они будут доверять нам.Легко, правда?

Итак, сегодня мы рассмотрим несколько способов, с помощью которых мы можем не доверять нашим пользователям:

Проверка входных данных

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

Рассмотрите следующую форму:

Основная форма

Из фрагментов информации, собранной здесь, было бы очень просто предположить, что адрес электронной почты будет действительным адресом электронной почты (особенно если мы использовали поле ввода электронной почты HTML5), а страна будет действительной страной (поскольку это поле выбора).

Но вы не можете доверять своим пользователям, делая такие предположения. Пользователь может изменить HTML в браузере, чтобы эти поля ввода принимали абсолютно все.

Вместо этого нам нужно четко указать нашу проверку ввода:

  • First / last name
    • Required field
    • Must be a string
    • Max length 255
  • Email address
    • Required field
    • Must be a string
    • Must be a recognisable email address format
    • Max length 255
    • Must not be used by another user in the database
  • Country
    • Required field
    • Must be in a pre-defined list of allowed countries

Теперь, когда мы определили явные правила, мы можем использовать невероятно мощную компонент проверки для проверки ввода, чтобы убедиться, что это именно то, что мы ожидаем.

$data = $request->validate([
    'first_name' => ['required', 'string', 'max:255'],
    'Last_name'  => ['required', 'string', 'max:255'],
    'email'      => ['required', 'string', 'email', 'max:255', 'unique:users'],
    'country'    => ['required', Rule::in($this->allowedCountries())],
]);

Если проверка проходит успешно, мы знаем, что $data содержит значения, соответствующие нашим ожиданиям, которые можно безопасно хранить в базе данных и аккуратно использовать во всем приложении.Страна безопасна для использования повсюду, поскольку она будет точно соответствовать нашему списку разрешенных, и мы знаем, что адрес электронной почты имеет допустимый формат, поэтому мы можем начать отправлять на него уведомления.

Имя/фамилия, с другой стороны, мы перейдем к следующему.

Прежде чем двигаться дальше, важно отметить, что валидатор будет возвращать только те ключи, которые были включены в правила проверки. Это означает, что любые дополнительные данные, отправленные в форму, будут проигнорированы, что позволит безопасно передавать данные непосредственно в модель с помощью create(), fill() или update(), избегая так называемой уязвимости массового назначения. Я всегда храню данные в своих моделях именно так.

Параметризированные запросы

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

В Laravel мы обычно пишем наши запросы следующим образом:

$name = $request->query(‘name');
$user = DB::table('users')->where('name', $name)->get();

Ключевым компонентом здесь является метод where(), где второй параметр ($name) поступает непосредственно из пользовательского ввода. Laravel автоматически включает этот второй параметр как параметризованный запрос, который предотвращает внедрение SQL, передавая его непосредственно в базу данных.Это одна из замечательных особенностей Laravel, которая затрудняет непреднамеренное создание уязвимых запросов.

Но что, если вам нужен действительно сложный запрос или вы хотите использовать некоторую логику, специфичную для ядра базы данных? Или вам просто нужно выполнить запрос, который гораздо эффективнее писать на чистом SQL?

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

Рассмотрите этот код:

public function store(Request $request)
{
    $data = $request->validate([
        'game' => ['required'],
        'date' => ['required'],
    ])
 
    DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = {$data['game']}");
}

Когда разработчик писал этот код, он ожидал, что $data['game'] будет целым числом, когда оно будет введено в запрос (и они прочитали это необработанное целочисленное сравнение быстрее!).Да, через браузер пропустили, но они посчитали, что это скрытое поле и никто его не заметит! Но это все еще поле ввода, и хакер может изменять его сколько угодно...

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

В этом случае мы можем сделать что-то вроде этого:

DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = ?", [$data['game']]);

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

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

Если вы хотите глубже изучить параметризованные запросы, я написал о них на странице Laravel Security in Depth, и я бы рекомендовал проверить официальную базу данных Laravel и документацию Eloquent.

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

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

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

42 && user_id = (SELECT id FROM users WHERE email = '[email protected]' LIMIT 1)

Отправив это в приложение, вы получите следующий SQL-запрос:

DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = 42 && user_id = (SELECT id FROM users WHERE email = `[email protected]` LIMIT 1)");

Немного почистил:

UPDATE game_user
SET turns += event_increment
WHERE game_id = 42
  AND user_id = (
    SELECT id
    FROM users
    WHERE email = '[email protected]'
    LIMIT 1
  )

И все.Мои ходы будут увеличиваться, но никто другой. Очень просто, и все из-за отсутствия проверки и параметризации запросов.

Это тривиальный пример, но вы можете сделать МНОГОЕ с помощью SQL Injection. Существует множество приемов для извлечения информации, в том числе когда страница не возвращает никакой видимой обратной связи (или сообщений об ошибках!).Если вы хотите узнать больше, в октябре мы углубились в SQL Injection в Laravel Security in Depth, включая преднамеренно уязвимое веб-приложение, где вы можете проводить свои собственные SQLi-атаки. Посмотрите здесь.

Экранирование выходных данных

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

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

Эти вещи здесь:

{!! $variable !!}

Пожалуйста, просто не используйте их.

Вы должны помнить о том, что вы выводите на страницу.Рассмотрим наш ввод ранее, с именем и фамилией для пользователя.

Что, если пользователь указал это как свое имя:

Stephen <script src="https://evilhacker.dev/evil.js"></script>

А затем мы сделали это на странице:

{{ $user->first_name }}

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

Но что, если бы наш код выглядел так:

// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">{$user->first_name}</a>")
    ->join(', ', ' and ');
 
return view('pages', ['links' => $links])
// Template
<div>
    {!! $links !!}
</div>

Все, что мы увидим, это «Стивен», а вредоносный Javascript будет запущен в браузере и будет делать все, что хочет хакер. Вы можете легко понять, почему разработчик выбрал это решение, но оно оставляет приложение широко открытым для XSS.

Итак, как мы это делаем?

  1. We have the e($value) function, which does the actual escaping of output when you use the {{ $value }} tags. You can call it anywhere.
  2. If you wrap something inside the Illuminate\Support\HtmlString class, it will bypass escaping.
// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">".e($user->first_name)."</a>")
    ->join(', ', ' and ');
 
return view('pages', ['links' => new HtmlString($links)])
// Template
<div>
    {{ $links }}
</div>

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297