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

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, основанные на чёрных списках, всегда умудряются не замечать опасные комбинации кода. А средства, использующие белые списки, устраняют эту неопределённость, допуская только известные разрешённые элементы и атрибуты. Все остальные будут просто отделяться, изолироваться или удаляться, вне зависимости от того, чем они являются.

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

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


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

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

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

ipt>alert(document.cookie);ipt>

В этом примере простая фильтрация по тэгу ничего не даст: удаление явного тэга