Pull to refresh
VK
Building the Internet

Книга «Как пережить полный конец обеда, или безопасность в PHP». Часть 1

Reading time 22 min
Views 45K
Original author: Padraic Brady
image

Big Five Part 3 by CrazyAsian1

Привет. Меня зовут Саша Баранник. В Mail.Ru Group я руковожу отделом веб-разработки, состоящим из 15 сотрудников. Мы научились создавать сайты для десятков миллионов пользователей и спокойно справляемся с несколькими миллионами дневной аудитории. Сам я занимаюсь веб-разработкой около 20 лет, и последние 15 лет по работе программировать приходится преимущественно на PHP. Хотя возможности языка и подход к разработке за это время сильно изменились, понимание основных уязвимостей и умение от них защититься остаются ключевыми навыками любого разработчика.

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

P. S. Книга длинная, поэтому перевод будет выкладываться несколькими статьями. Итак, приступим…

Ещё одна книга по безопасности в PHP?


Есть много способов начать книгу про безопасность в PHP. К сожалению, я не читал ни одной из них, так что придётся разобраться с этим в процессе написания. Пожалуй, я начну с самого основного и буду надеяться, что всё получится.

Если рассмотреть абстрактное веб-приложение, запущенное в онлайне компанией Х, то можно предположить, что оно содержит ряд компонентов, которые, если их взломать, способны нанести существенный вред. Какой, например?

  1. Вред пользователям: получение доступа к электронной почте, паролям, персональным данным, реквизитам банковских карт, деловым тайнам, спискам контактов, истории транзакций и глубоко охраняемым секретам (вроде того, что кто-то назвал свою собаку Блёсткой). Утечка этих данных вредит пользователям (частным лицам и компаниям). Навредить могут также веб-приложения, неправильно применяющие подобные данные, и узлы, которые используют доверие пользователей в своих интересах.
  2. Вред самой компании X: из-за причинённого пользователям ущерба ухудшается репутация, приходится выплачивать компенсации, теряется важная деловая информация, возникают дополнительные расходы — на инфраструктуру, улучшение безопасности, ликвидацию последствий, судебные издержки, крупные пособия увольняемым топ-менеджерам и т. д.

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

К сожалению, вопросы безопасности очень часто решаются задним числом. Считается, что самое важное — создать работающее приложение, отвечающее нуждам пользователей, с приемлемым бюджетом и сроком. Это вполне понятный набор приоритетов, но нельзя вечно игнорировать безопасность. Гораздо лучше постоянно держать её в уме, внедряя конкретные решения в ходе разработки, когда стоимость изменений ещё невелика.

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

Поскольку система безопасности веб-приложения должна защищать пользователей, которые доверяют сервисам приложения, необходимо знать ответы на вопросы:

  1. Кто хочет нас атаковать?
  2. Как они могут нас атаковать?
  3. Как мы можем их остановить?

Кто хочет нас атаковать?


Ответ на первый вопрос очень прост: все и всё. Да, вся Вселенная хочет вас проучить. Пацан с разогнанным компом, на котором запущен Kali Linux? Вероятно, он вас уже атаковал. Подозрительный мужчина, любящий вставлять палки в колёса? Вероятно, он уже нанял кого-то, чтобы вас атаковали. Доверенный REST API, через который вы ежечасно получаете данные? Вероятно, он был хакнут ещё месяц назад, чтобы подкидывать вам заражённые данные. Даже я могу атаковать вас! Так что не нужно слепо верить этой книге. Считайте, что я лгу. И найдите программиста, который выведет меня на чистую воду и разоблачит мои вредные советы. С другой стороны, может быть, он тоже собирается вас хакнуть…

Смысл этой паранойи в том, чтобы было проще мысленно разделить на категории всё, что взаимодействует с вашим веб-приложением («Пользователь», «Хакер», «База данных», «Ненадёжный ввод», «Менеджер», «REST API»), а затем присваивать каждой категории индекс доверия. Очевидно, что «Хакеру» доверять нельзя, но что насчёт «Базы данных»? «Ненадёжный ввод» получил своё название не просто так, но вы действительно станете фильтровать пост в блоге, полученный из доверенной Atom-ленты своего коллеги?

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

Вернёмся к «Базам данных». Если предположить, что хакер сможет получить доступ к базе (а мы, параноики, всегда это предполагаем), то ей никогда нельзя доверять. Большинство приложений доверяют базам безо всяких вопросов. Снаружи веб-приложение выглядит единым целым, но внутри оно представляет из себя систему из отдельных компонентов, обменивающихся данными. Если считать все эти компоненты доверенными, то при взломе одного из них быстро окажутся скомпрометированы и все остальные. Такие катастрофические проблемы в безопасности не получится решить фразой «Если база взломана, то мы всё равно проиграли». Вы можете так сказать, но вовсе не факт, что вам придётся это делать, если изначально не будете доверять базе и действовать соответствующе!

Как они могут нас атаковать?


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

Если вы используете источники данных без проверки того, полностью ли безопасны эти данные и подходят ли они для использования, то вы потенциально открыты для атаки. Также необходимо проверять, что полученные данные соответствуют данным, которые вы отправляете. Если данные не сделаны полностью безопасными для вывода, то у вас тоже будут серьёзные проблемы. Всё это можно выразить в виде правила для PHP «Проверяйте ввод; экранируйте вывод».

Это очевидные источники данных, которые мы должны как-то контролировать. Также к источникам могут относиться хранилища на стороне клиента. Например, большинство приложений распознают пользователей, присваивая им уникальные ID сессии, которые могут храниться в куках. Если атакующий заполучит значение из куки, то он может выдать себя за другого пользователя. И хотя мы можем уменьшить некоторые риски, связанные с перехватом или подделкой пользовательских данных, гарантировать физическую безопасность компьютера пользователя мы не в состоянии. Мы даже не можем гарантировать, что юзеры сочтут «123456» глупейшим паролем после «password». Дополнительную пикантность придаёт факт, что сегодня куки не единственный вид хранилища на стороне пользователя.

Ещё один риск, часто упускаемый из виду, касается целостности вашего исходного кода. В PHP становится всё популярнее разработка приложений на основе большого количества слабо связанных друг с другом библиотек, модулей и пакетов для фреймворков. Многие из них скачиваются из общественных репозиториев, таких как Github, ставятся с помощью установщиков пакетов вроде Composer и его веб-компаньона Packagist.org. Поэтому безопасность исходного кода полностью зависит от безопасности всех этих сторонних сервисов и компонентов. Если окажется скомпрометирован Github, то, скорее всего, он будет использован для раздачи кода с вредоносной добавкой. Если Packagist.org — то атакующий сможет перенаправлять запросы пакетов на свои, вредоносные пакеты.

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

Как мы можем их остановить?


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

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

Базовые принципы безопасности


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

  1. Не верьте никому и ничему.
  2. Всегда предполагайте худший сценарий.
  3. Применяйте многоуровневую защиту (Defence-in-Depth).
  4. Придерживайтесь принципа «чем проще, тем лучше» (Keep It Simple Stupid, KISS).
  5. Придерживайтесь принципа «минимальных привилегий».
  6. Злоумышленники чуют неясность.
  7. Читайте документацию (RTFM), но никогда ей не доверяйте.
  8. Если это не тестировали, то это не работает.
  9. Это всегда ваша ошибка!

Давайте кратко пробежимся по всем пунктам.

1. Не верьте никому и ничему


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

2. Всегда предполагайте худший сценарий


У многих систем безопасности есть общее свойство: не важно, насколько хорошо они сделаны, каждая может быть пробита. Если вы будете это учитывать, то быстро поймёте преимущество второго пункта. Ориентирование на худший сценарий поможет оценить обширность и степень вредоносности атаки. А если она и впрямь произойдёт, то, возможно, вам удастся уменьшить неприятные последствия благодаря дополнительным средствам защиты и изменениям в архитектуре. Быть может, традиционное решение, которое вы используете, уже заменили чем-то лучшим?

3. Применяйте многоуровневую защиту (Defence-in-Depth)


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

4. Придерживайтесь принципа «чем проще, тем лучше» (Keep It Simple Stupid, KISS)


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

5. Придерживайтесь принципа «минимальных привилегий»


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

6. Злоумышленники чуют неясность


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

7. Читайте документацию (RTFM), но никогда ей не доверяйте


Руководство по PHP — это Библия. Конечно, оно не было написано Летающим Макаронным Монстром, так что формально может содержать какое-то количество полуправды, недочётов, неверных толкований или ошибок, пока ещё не замеченных или не исправленных. То же самое касается и Stack Overflow.

Специализированные источники мудрости в сфере безопасности (ориентированные на PHP и не только) в целом дают более подробные знания. Ближе всего к Библии по безопасности в PHP находится сайт OWASP с предлагаемыми на нём статьями, руководствами и советами. Если на OWASP что-то не рекомендуется делать — никогда не делайте!

8. Если это не тестировали, то это не работает


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

9. Это всегда ваша ошибка!


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

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

В то же время атаки на системы безопасности часто представляют собой комбинации атак. По отдельности они малозначимы, но при этом иногда открывают дорогу другим атакам. Например, для внедрения SQL-кода иногда требуется имя конкретного пользователя, которое можно получить с помощью атаки по времени (Timing Attack) против административного интерфейса, вместо куда более дорогого и заметного брутфорса. В свою очередь, внедрение SQL позволяет реализовать XSS-атаку на конкретный административный аккаунт, не привлекая внимания большим количеством подозрительных записей в логах.

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

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

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


Проверка входных данных — это внешний оборонный периметр вашего веб-приложения. Он защищает основную бизнес-логику, обработку данных и генерацию выходных данных. В буквальном смысле всё за пределами этого периметра, за исключением кода, исполняемого текущим запросом, считается вражеской территорией. Все возможные входы и выходы периметра день и ночь охраняются воинственными часовыми, которые сначала стреляют, а потом задают вопросы. К периметру подключены отдельно охраняемые (и очень подозрительно выглядящие) «союзники», включая «Модель», «Базу данных» и «Файловую систему». Никто в них стрелять не хочет, но, если они попытаются испытать судьбу… бах. У каждого союзника есть свой собственный периметр, который может доверять или не доверять нашему.

Помните мои слова о том, кому можно доверять? Никому и ничему. В мире PHP везде встречается совет не доверять «вводимым пользователем данным». Это одна из категорий по степени доверия. Предполагая, что пользователям доверять нельзя, мы думаем, что всему остальному доверять можно. Это не так. Пользователи — наиболее очевидный ненадёжный источник входных данных, потому что мы их не знаем и не можем ими управлять.

Критерии проверки


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

filter_var('php://example.org', FILTER_VALIDATE_URL);

Фильтр проходится без вопросов. Проблема в том, что принятый URL php:// может быть передан PHP-функции, которая ожидает получения удалённого HTTP-адреса, а не возвращения данных от исполняемого PHP-скрипта (через PHP-обработчик). Уязвимость возникает потому, что опция фильтрации не имеет метода, ограничивающего допустимые URI. Несмотря на то, что приложение ожидает ссылки http, https или mailto, а не какой-то URI, характерный для PHP. Нужно всеми способами избегать подобного, слишком общего подхода к проверке.

Будьте осторожны с контекстом


Проверка входных данных должна предотвращать ввод в веб-приложение небезопасных данных. Серьёзный камень преткновения: проверка на безопасность данных обычно выполняется только для первого предполагаемого использования.

Допустим, я получил данные, содержащие имя. Я достаточно просто могу проверить его на наличие апострофов, дефисов, скобок, пробелов и целого ряда алфавитно-цифровых Unicode-символов. Имя — это корректные данные, которые могут использоваться для отображения (первое предполагаемое использование). Но если использовать его где-то ещё (например, в запросе в базу данных), то оно окажется в новом контексте. И некоторые из символов, которые допустимы в имени, в этом контексте окажутся опасными: если имя преобразуется в строку для выполнения SQL-инъекции.

Получается, что проверка входных данных по сути ненадёжна. Она наиболее эффективна для отсечения однозначно недопустимых значений. Скажем, когда что-то должно быть целым числом, или буквенно-цифровой строкой, или HTTP URL. Такие форматы и значения имеют свои ограничения и при должной проверке с меньшей вероятностью будут представлять угрозу. Другие значения (неограниченный текст, GET/POST-массивы и HTML) проверять труднее, и вероятность получения зловредных данных в них выше.

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

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

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

Хотя это может восприниматься как дублирование валидации при первоначальном вводе, на самом деле дополнительные стадии проверки лучше учитывают особенности текущего контекста, когда требования к данным сильно отличаются. Например, поступающие из формы данные могут содержать процентное число. При первом использовании мы проверяем, что значение действительно целочисленное. Но при передаче в модель нашего приложения могут возникнуть новые требования: значение должно укладываться в определённый диапазон, обязательный для работы бизнес-логики приложения. И если в новом контексте не будет выполняться эта дополнительная проверка, то могут возникнуть серьёзные проблемы.

Используйте только белые списки, а не чёрные


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

Хороший пример — любая процедура проверки, призванная сделать HTML безопасным с точки зрения неэкранированных выходных данных в шаблоне. Если использовать чёрный список, то нам нужно проверить, чтобы HTML не содержал опасных элементов, атрибутов, стилей и исполняемого JavaScript. Это большой объём работы, и средства очистки HTML, основанные на чёрных списках, всегда умудряются не замечать опасные комбинации кода. А средства, использующие белые списки, устраняют эту неопределённость, допуская только известные разрешённые элементы и атрибуты. Все остальные будут просто отделяться, изолироваться или удаляться, вне зависимости от того, чем они являются.

Так что белые списки предпочтительнее для любых процедур проверки благодаря более высокой безопасности и надёжности.

Никогда не пытайтесь исправлять входные данные


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

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

Одно из последствий попытки исправить входные данные: атакующий может предсказать влияние ваших исправлений. Допустим, есть некое недопустимое строковое значение. Вы его ищете, удаляете и завершаете фильтрацию. А что если злоумышленник создаст значение, разделённое строкой, чтобы перехитрить ваш фильтр?

<scr<script>ipt>alert(document.cookie);</scr<script>ipt>

В этом примере простая фильтрация по тэгу ничего не даст: удаление явного тэга <script> приведёт к тому, что данные будут считаться полностью валидным элементом HTML-скрипта. То же самое можно сказать и про фильтрацию по любым конкретным форматам. Всё это наглядно показывает, почему нельзя делать проверку входных данных последним защитным контуром приложения.

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

Никогда не доверяйте средствам внешней проверки и постоянно отслеживайте уязвимости


Ранее я отмечал, что проверка необходима при каждой передаче данных в новый контекст. Это применимо и к проверке, выполняемой вне самого веб-приложения. К подобным средствам относится проверка или иные ограничения, применяемые к HTML-формам в браузере. Посмотрите на эту форму из HTML 5 (метки опущены):

<form method="post" name="signup">
    <input name="fname" placeholder="First Name" required />
    <input name="lname" placeholder="Last Name" required />
    <input type="email" name="email" placeholder="someone@example.com" required />
    <input type="url" name="website" required />
    <input name="birthday" type="date" pattern="^d{1,2}/d{1,2}/d{2}$" />
    <select name="country" required>
        <option>Rep. Of Ireland</option>
        <option>United Kingdom</option>
    </select>
    <input type="number" size="3" name="countpets" min="0" max="100" value="1" required />
    <textarea name="foundus" maxlength="140"></textarea>
    <input type="submit" name="submit" value="Submit" />
</form>

HTML-формы умеют накладывать ограничения на вводимые данные. Вы можете ограничивать выбор с помощью списка из фиксированных пунктов, задавать минимальные и максимальные значения, а также ограничивать длину текста. Возможности HTML 5 ещё шире. Браузеры могут проверять URL и адреса почты, контролировать даты, числа и диапазоны (хотя поддержка последних двух довольно условна). Также браузеры способны проверять вводимые данные с помощью регулярных выражений JavaScript, включённых в атрибут шаблона.

Со всем этим обилием средств контроля нельзя забывать: их предназначение — улучшить удобство вашего приложения. Любой злоумышленник способен создать форму, которая не будет содержать ограничений из вашей исходной формы. Можно даже создать HTTP-клиент для автоматизированного заполнения форм!

Другой пример внешних средств проверки — получение данных от сторонних API, например Twitter’а. Эта соцсеть обладает хорошей репутацией, и ей обычно доверяют без вопросов. Но раз уж мы параноики, то не стоит доверять даже Twitter’у. При компрометации в его ответах появятся небезопасные данные, к появлению которых мы не будем готовы. Поэтому даже здесь применяйте собственную проверку, чтобы не оказаться беззащитными в случае чего.

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

Избегайте преобразования типов в PHP


PHP не строго типизированный язык, и большинство его функций и операций типизируются небезопасно. Это может привести к серьёзным проблемам. Причём особенно уязвимы не сами значения, а валидаторы. Например:

assert(0 == '0ABC'); //возвращает TRUE
assert(0 == 'ABC'); //возвращает TRUE (даже без цифры в начале!)
assert(0 === '0ABC'); //возвращает NULL/Выдаёт предупреждение о невозможности проверить утверждение

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

function checkIntegerRange($int, $min, $max) {
    if (is_string($int) && !ctype_digit($int)) {
        return false; // содержит нецифровые символы
    }
    if (!is_int((int) $int)) {
        return false; // другое не целое значение или больше PHP_MAX_INT
    }
    return ($int >= $min && $int <= $max);
}

Никогда не делайте так:

function checkIntegerRangeTheWrongWay($int, $min, $max) {
    return ($int >= $min && $int <= $max);
}

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

assert(checkIntegerRange("6' OR 1=1", 5, 10)); //вернёт NULL/Выдаст предупреждение
assert(checkIntegerRangeTheWrongWay("6' OR 1=1", 5, 10)); //ошибочно вернёт TRUE

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

Виды проверки данных


Ошибки в проверке входных данных приводят к появлению уязвимостей и повреждению данных. Ниже на примере PHP мы рассмотрим ряд видов проверки.

Проверка типа данных


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

Проверка на допустимость символов


Мы проверяем, чтобы строка содержала только допустимые символы. Чаще всего для этого в PHP используются ctype-функции, а для более сложных случаев — регулярные выражения. Если вам нужны только ASCII-символы, то лучше всего остановиться на ctype-функциях.

Проверка формата


Это позволяет удостовериться, что данные соответствуют конкретному набору допустимых символов. Яркие примеры — электронная почта, URL и даты. Лучше всего применять функцию PHP filter_var(), класс DateTime, а для других форматов — регулярные выражения. Чем сложнее формат, тем больше нужно склоняться к надёжным инструментам для проверки формата или синтаксиса.

Проверка ограничений


Здесь проверяется, входит ли значение в разрешённый диапазон. Например, нам нужно принимать только значения больше 5, или от 0 до 3, или не равные 34. Проверка ограничений может применяться также к строкам, к размерам файлов, разрешениям изображений, диапазонам дат и т. д.

Проверка на наличие данных


Мы проверяем, все ли необходимые для дальнейшей работы данные имеются. Например, в форме регистрации это логин, пароль и адрес почты. Если что-то не введено, то считаем данные некорректными.

Проверка на совпадение данных


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

Логическая проверка


По сути это контроль ошибок, когда мы хотим удостовериться, что полученные данные не приведут к ошибке или исключению в приложении. Например, при подстановке в регулярное выражение полученной поисковой строки может произойти ошибка компиляции выражения. Опасны могут быть и целые числа, превышающие какое-то значение, ноль в знаменателе при операции деления или странности наподобие +0, 0 и –0.

Проверка на наличие ресурса


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

Проверка источников входных данных


Несмотря на наши усилия, проверка входных данных не решает всех проблем безопасности. Очень часто не удаётся достоверно проверить вводимую пользователями информацию. Вероятность этого повышается, когда приложение работает с источниками данных, которые считаются доверенными (например, с локальной базой данных). В случае с базой данных дополнительных видов проверок не так много. Но давайте рассмотрим пример удалённого веб-сервиса, защищённого с помощью SSL или TLS, например, когда мы через HTTPS запрашиваем информацию у API.

HTTPS — это основной способ защиты от атак «человека посередине» (Man-In-The-Middle, MITM), когда злоумышленник становится посредником между двумя точками обмена данными. Такой посредник выдаёт себя за сервер. То есть клиенты считают, что подключаются к серверу; на самом деле это злоумышленник, который создаёт отдельное подключение к запрашиваемому серверу. Атакующий ретранслирует данные в обоих направлениях и может их считывать, причём ни клиенты, ни сервер об этом не подозревают. Более того, посредник в силах изменять данные в ходе ретрансляции.

Для предотвращения таких атак необходимо закрыть злоумышленникам возможность выдавать себя за сервер и читать сообщения, которыми обмениваются сервер и клиенты. Для этого существует SSL/TLS, выполняющий две основные функции защиты:

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

Имейте в виду, что шифрование с помощью SSL/TLS может происходить между двумя любыми сторонами. При атаке «человек посередине» клиент связывается с атакующим «сервером» и начинает обсуждать использование взаимного шифрования данных. В данном случае оно само по себе бесполезно, потому что мы не просили «сервер» доказать, что он именно тот, за кого себя выдаёт. Поэтому необходим второй этап работы SSL/TLS, формально опциональный. Веб-приложение ОБЯЗАНО проверять идентификацию сервера, с которым оно общается, чтобы защититься от атаки «человек посередине».

Широко распространено мнение, что для защиты от подобных атак достаточно одного лишь шифрования, и многие приложения и библиотеки не используют второй этап. Это частая и легко обнаруживаемая уязвимость в приложениях с открытым кодом. По каким-то непостижимым причинам сам PHP по умолчанию отключает проверку серверов в своей HTTPS-обёртке, когда используются stream_socket_client(), fsockopen() или другие внутренние функции. Например:

$body = file_get_contents('https://api.example.com/search?q=sphinx');

Здесь очевидна уязвимость для атаки «человек посередине». Любые данные из этого HTTPS-запроса никогда нельзя рассматривать как полученные от необходимого нам сервиса. По уму, запрос должен выполняться с проверкой сервера:

$context = stream_context_create(array('ssl' => array('verify_peer' => TRUE)));
$body = file_get_contents('https://api.example.com/search?q=sphinx', false, $context);

UPD. В PHP 5.6+ опция ssl.verify_peer по умолчанию установлена в TRUE.

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

curl_setopt(CURLOPT_SSL_VERIFYPEER, false);

Отключение проверки сервера в контексте SSL или при использовании curl_setopt() приведёт к уязвимости к атакам «человек посередине». Но её отключают как раз для того, чтобы решить проблему назойливых ошибок, которые свидетельствуют об атаке или о попытках приложения связаться с хостом, чей SSL-сертификат сконфигурирован неправильно или просрочен.

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

Выводы


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

Но хакеры тоже не зря едят свой хлеб. Они ищут новые возможности обойти наши несовершенные защиты и изучают уязвимости в используемых нами модулях и библиотеках. И если нашей задачей является создать защищенное веб-приложение, то их — скомпрометировать наши сервисы и данные. В конечном счёте все мы работаем на улучшение наших продуктов.
Tags:
Hubs:
+55
Comments 19
Comments Comments 19

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен