О проблемах информационной безопасности и IT образования на примере HTML Academy
Меня всегда очень интересовала довольно грустная ситуация с языком РНР. Из неказистого шаблонного движка для веб-страничек, к середине 2010-х он вырос в мощный, современный и аккуратный язык программирования… в то время как практически все обучающие материалы в сети выставляют его всё тем же неуклюжим уродцем, который с огромным трудом, не соблюдая никаких стандартов, позволяет разве что сделать примитивную веб-страничку с кучей уязвимостей. Что, разумеется, уже давно совершенно не так. Поэтому когда на форуме РНР клуба появился пост о наборе «наставников» на курс по РНР в HTML Academy, я не раздумывая подал заявку. Чтобы посмотреть как с обстоит с этим дело на платных курсах, а так же по возможности поделиться своим опытом в этой области.
Что вам сказать? «Если хотите, чтобы вам и дальше нравилась колбаса, не берите экскурсию на мясокомбинат»
Предуведомление. Все мои впечатления относятся к курсу «PHP. Профессиональная веб-разработка». У меня нет информации о других курсах и технологиях, которые преподают в Академии. Есть только общее ощущение, что с технологиями front-end ситуация вроде должна быть получше.
Несмотря на то, что курс в целом составлен довольно неплохо, он, к сожалению, изобилует ошибками и неточностями. Чтобы не растекаться мыслью по древу, я не буду останавливаться на мелочах, а сконцентрируюсь на одной теме — безопасности, поскольку она остаётся больным местом всех руководств по языку, и платные курсы — как выяснилось — не являются здесь, увы, исключением. Учитывая важность темы, я планирую подробно разобрать все случаи, объяснить причины заблуждений и показать правильные решения.
По этой причине первая часть будет изобиловать техническими подробностями, которые я по возможности буду убирать под спойлер. Но отказываться от них совсем мне не хочется, чтобы статья не превратилась в очередной набор голословных утверждений. Кроме того, я хочу чтобы статья была не столько критической, сколько полезной — для тех, кто только начинает осваивать веб-программирование, причём не только на РНР: я встречался с перечисленными ниже проблемами и в других языках. И я надеюсь что приведённый ниже разбор позволит читателям разложить по полочкам основные вопросы безопасности, и причесать тот сумбур, который остаётся после прочтения разрозненных и часто противоречивых рекомендаций, приводимых в интернете.
Забегая вперёд скажу, что мне так и не удалось добиться исправления перечисленных ниже проблем в материалах курса, о причинах чего я попробую порассуждать во второй части.
Пробелы в освещении вопросов безопасности в материалах курса не являются чем-то новым, а наоборот — коллекцией типичных заблуждений, кочующих по руководствам вида «РНР за 24 часа» и видеокурсам. Тем печальнее их видеть за фасадом вроде бы респектабельного учебного заведения, активно пиарящего свои курсы на Хабре.
SQL инъекция
Для иллюстрации я приведу код из реального учебного проекта, который студент успешно защитил:
$id = mysqli_real_escape_string($con, $_GET['id']);
$query = "select * from lots where id = ".$id;
$result = mysqli_query($con, $query);
Люди, знакомые с темой SQL инъекций, уже отбивают себе лицо ладонями, а для остальных поясню: данный код — это типичнейший пример карго культа (en), самолёт из соломы. Ритуальное выполнение неких действий без понимания их смысла и — совершенно закономерно — без ожидаемого результата.
Здесь мы видим два совершенно хрестоматийных примера:
- собственно пример кода, уязвимого к SQL инъекции
- а так же пример одного из самых заскорузлых заблуждений в мире РНР — что функция
mysqli_real_escape_string()
— это такая волшебная палочка, которая неким магическим образом защищает нас от любых инъекций — стоит только помахать ей в воздухе
Причём это заблуждение напрямую транслируется в учебник:
При использовании mysqli_real_escape_string значение в итоге преобразуется к безопасному виду
То есть студент здесь не проявляет никакой самодеятельности, а строго следует указаниям учебных материалов. Как и проверяющие, которые не увидели в этом коде ничего предосудительного, оставив этот код без внимания как при проверке промежуточного задания, так и при защите финального проекта: студент успешно закончил курс с отличием.
Почему этот код уязвим? Функция mysqli_real_escape_string()
, как следует из её названия, работает только со строками, и применять её для числовых значений бесполезно чуть более чем полностью. Если бы переменная $id
была взята в кавычки, то да — этот код был бы безопасным. Но поскольку их нету, а функция экранирования, по большому счёту, только для них и нужна, то в результате мы невозбранно можем писать любой SQL, если только в нем не требуются кавычки.
В итоге мы имеем код, уязвимый к SQL инъекции. Но тема инъекций всегда ощущается неполной, если не приводится конкретный пример взлома.
Раскапывая тему инъекций, всегда сталкиваешься, а тем, что реальность немного отличается от расхожих представлений о ней. В частности, классический пример с мальчиком по имени Bobby Tables практически никогда неприменим на практике, поскольку большинство функций для работы с БД не позволяют выполнить больше одного запроса за раз. Так же не премину отметить, что в этой популярной карикатуре тоже написана чушь про «экранирование символов».
Ну то есть в теории понятно, через эту дырочку можно слить всю информацию из базы данных, но вот как это сделать конкретно? И здесь нас ждёт интересный момент: очень важно не путать понятие инъекции как уязвимости, и как конкретного эксплойта. Уязвимость — это сама возможность модифицировать код SQL, манипулируя входящими данными. Она тут уже есть. А вот эксплойт — это уже собственно конкретная эксплуатация этой уязвимости, и над ней придётся попотеть.
То есть очень важно понимать, что даже если один конкретный известный вам эксплойт не подошёл, это не значит, что ваш код безопасен. Синтаксис SQL велик и многообразен — вы даже не представляете, какие там есть варианты для обхода различных ограничений. Из чего надо сделать вывод, что нам важен сам факт наличия уязвимости. Если она есть, то хакеры постоянно будут пытаться подобрать подходящий синтаксис SQL, чтобы выполнить вредоносный запрос — и в конце концов преуспеют. Однако я понимаю, что такие рассуждения обычно выглядят разговорами в пользу бедных, и своим студентам я всегда стараюсь наглядно показать, как можно в реальности пострадать от кода, который они пишут.
Для того, чтобы слить всю информацию из БД, в первую очередь нам надо выяснить количество полей, которое возвращает исходный запрос. Это мы делаем через UNION, последовательно подставляя различное количество полей в присоединённый запрос. Для этого в `$_GET['id'] передаём такую строку, меняя в ней количество цифр до тех пор, пока запрос не перестанет выдавать ошибку:
1 union select 1,2,3,4,5 from users limit 1
что в итоге даст нам запрос
select * from lots where id = 1 union select 1,2,3,4,5 from users limit 1
как можно заметить, ни одной кавычки в этом запросе нет, так что пресловутая mysqli_real_escape_string позволяет нам его выполнить без проблем. Как только запрос перестаёт падать с ошибкой — мы получили нужное количество полей. Теперь мы можем получить из БД information_schema список всех таблиц в базе данных или список всех полей в какой-то таблице. Первый шаг мы пропустим, и сразу перейдём ко второму — предположим, что мы уже узнали, что в БД есть таблица users. Для этого выберем поле, которое без изменений выводится на экран, и подставим вместо цифры подзапрос в БД information_schema
select * from bets where id=0 union select 1,(select group_concat(column_
name) from information_schema.columns where table_schema=database() and table_na
me=0x7573657273),3,4,5 from users limit 1
Только надо будет указать несуществующий id — чтобы исходный запрос не вернул ни одной строки, и в результат пошли данные из присоединённого запроса. Плюс, как можно увидеть, для того, чтобы избежать использования кавычек, имя таблицы мы передаём через hex-кодирование.
Получив таким образом список полей в таблице users, их уже можно будет подставить в UNION запрос и получить на экране информацию об интересующем пользователе.
И ещё один очень важный момент. При эксплуатации этой уязвимости мы использовали сообщения об ошибках. Если количество колонок в двух UNION запросах не совпадает, то БД любезно сообщит нам об этом. И здесь очень важно помнить, что на боевом сервере системные сообщения об ошибках ни в коем случае не должны выводиться на экран.
Как знают все настоящие программисты, сообщения об ошибках — это совершенно незаменимые помощники. Они показывают — где, и как именно мы накосячили в коде, попутно сообщая кучу всякой дополнительной информации. Разумеется, то же самое верно и в случае попыток взлома, но с обратным знаком — если ошибки SQL выводятся прямо на экран, то в этом случае они помогают уже атакующему, которому становится гораздо легче добиться нужного результата.
Именно поэтому ни в коем случае нельзя писать код, который всегда будет выводить ошибки на экран. Информирование об ошибках должно быть адаптивным, зависеть от окружения — в локальном/тестовом окружении можно и нужно выводить ошибки на экран, но вот на боевом сервере ошибки должны писаться только в лог. Но сожалению, в материалах курса ошибки как раз безусловно выводятся на экран, как это было принято в прошлом веке. Кто-то скажет — «ну не велика беда, это же учебный проект!» Я на это отвечу — в учебных проектах такие косяки стократ хуже. Человек с самого начала приучается делать неправильно — и дальше он либо будет продолжать косячить, либо ему придётся переучиваться. Я вот никогда не понимал такого подхода — сначала заведомо учиться делать неправильно, а потом переучиваться. Какой в этом смысл?
Тем более что современный РНР позволяет настроить драйвер БД таким образом, чтобы в случае ошибки SQL выбрасывалось исключение, управлять поведением которого можно централизованно, с помощью одной-двух конфигурационных настроек. То есть мы вообще не пишем никакого кода для обработки ошибок запросов, никак не проверяем результат выполнения — и при этом всегда узнаем об ошибке, если она произойдёт. Получаем win-win — пишем гораздо меньше кода, и получаем более гибкую обработку ошибок!
Что особенно пикантно — в отношении ошибок вообще, в учебнике пропагандируется правильный подход — ошибки не должны выводиться на экран. Но для ошибок SQL почему-то сделано исключение! Один из критериев успешности проекта буквально так и сформулирован —
С самым строгим уровнем репортинга ошибок (error_reporting = E_ALL) код личного проекта не должен вызывать никаких ошибок и предупреждений. Исключением являются ошибки, возникающие при использовании функций mysqli.
Так и хочется воскликнуть устами Вовочки из анекдота — «Где логика, где разум?»
Как правильно защищаться?
Должен признаться, что я немного слукавил с обличениями выше. На самом деле рассуждения про экранирование в учебнике относятся к разделу про защиту строк. То есть формально там написано всё правильно. Но тем не менее, это устаревший и ненадёжный подход — что мы и увидели наглядно в коде студента.
- все эти рассуждения даются уже после того, как студенту показывали многократно в разных видах, как поместить переменную прямо в запрос без всякой защиты
- все способы защиты, кроме подготовленных выражений, являются принципиально внешними для запроса, то есть их можно применить на довольно значительном расстоянии от него. Например, при помощи валидации входящих параметров. Это приводит к привычке не защищать сам запрос и — в конечном итоге — к инъекции. Потому что данные могут попадать в код разными путями, и не всегда валидируются вообще/валидируются правильно. Защита от инъекций должна проводиться в момент выполнения запроса, а это обеспечивают только подготовленные выражения.
- само количество разнообразных вариантов вносит сумбур в голову студента. В учебнике есть отдельно про то, выполнять запросы без всякой защиты, про то как защищать числа, отдельно про строки, отдельно про подготовленные выражения. И всё это вразнобой, без чёткой системы. И в итоге в голове у студента тоже не оказывается чёткой картинки, а только куча ненужных, зачастую противоречащих друг другу указаний, которые, во-первых, только запутывают его, а во-вторых, заставляют вывести какое-то простое правило, которого стоит всегда придерживаться… и разумеется, это правило — «всё экранировать»
Именно поэтому, вместо того чтобы путаться с кавычками, экранированием, строками, числами, кодировками, валидацией и прочим — разработчики всего мира уже давно выработали действительно безопасное универсальное правило: любые данные передавать в запрос только через знаки подстановки в подготовленных выражениях. В современном РНР (>= 8.1) это будут те же три строчки:
$stmt = $mysqli->prepare("SELECT MAX(amount) FROM bids WHERE lot_id = ?");
$stmt->execute([$_GET['id']]);
$result = $stmt->get_result();
Кода столько же, но при этом безопасность гарантирована. В более старых версиях добавится одна строчка с bind_param
.
Если с самого начала давать именно этот подход, то у студентов именно он будет откладываться на подкорке, именно его они будут применять по умолчанию. И как следствие — не будут позориться сами и позорить Академию на первом рабочем месте.
Важно: Подготовленные выражения надо использовать всегда, для любых данных. Вторым по распространённости заблуждением является то, что «защищать» надо только «пользовательский ввод» — и это полная, совершенно беспросветная чушь. Во-первых, далеко не всегда средний разработчик может сказать, какие данные пришли от пользователя, а какие — нет. Во-вторых, даже в самых супер-доверенных данных могут встречаться спец-символы SQL, и они просто поломают запрос. И в-третьих, это просто глупо — сидеть и сортировать данные, идущие в запрос: «тэээк-с, вот эти у нас опасные, их защищаем, а вот эти — нет». Защищать надо все. Любые данные предаются в запрос через знаки подстановки. Точка.
К сожалению, эта ересь про «пользовательский ввод» растиражирована вообще везде, включая таких тяжеловесов, как OWASP. Так что пнуть авторов учебника, в котором она повторяется, у меня не поднимется нога. Но тем не менее, это тоже очень важный момент, о котором надо всегда помнить.
Заливка шелла.
Эта уязвимость оказалась для меня сюрпризом. Если SQL инъекции в ученическом коде — это общее место, то заливка шелла — это уже заход с козырей, такое не часто встретишь.
Рекомендуемый учебником способ валидации заливаемых файлов — через функцию finfo_file (), которая определяет тип файла по его содержимому, а точнее — по нескольким первым байтам. В то время как веб-сервер определяет mime-тип файла по его расширению. В итоге в любом наугад взятом учебном проекте легко заливается файл с расширением .php
, а дальше веб-сервер его радостно исполняет.
Делаем файл gif_header.php
:
и выполняем команду
php gif_header.php > fakegif.php
И дальше можно в любом редакторе дописать в него любой payload, самый простейший —
Остаётся только создать папочку uploads и простейшую форму для заливки файлов, строго следующую рекомендациям из учебного курса:
после заливки файла переходим по адресу
http://localhost/uploads/fakegif.php?q=echo"hello!";
и наслаждаемся результатом
Ещё два года назад автору курса была наглядно продемонстрирована эта уязвимость, но так и не была исправлена. О причинах такого отношения мы поговорим во второй части.
Как правильно защищаться?
Тема довольно обширная, но как минимум надо просто добавить ещё и проверку на расширение, задавая его белым списком:
$file_type = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (!in_array($file_type, ["jpeg", "jpg", "png", "gif"], true)) {
$errors['imgage'] = 'Загрузите картинку в формате JPG, JPEG или PNG';
}
А так же, чтобы избежать довольно экзотической уязвимости сервера Apache, который расценивает file.php.jpg как файл РНР, ещё желательно и переименовывать картинку.
Также хочу сразу предупредить о полной бесполезности проверки $_FILES['image']['type'] — это значение присылает клиент, и может написать туда что угодно. Вот очень хорошая коротенькая статья с демонстрацией, Spoofing MIME types: Why you can’t trust the type field in $_FILES.
XSS
На фоне остальных проблем это уже кажется мелочью, но тем не менее, XSS — это тоже серьёзная уязвимость. Кроме того, она хорошо иллюстрирует один важный принцип, о котором я уже говорил выше, и который полностью игнорируется в учебнике — принцип обязательности применения защитных механизмов вне зависимости от источника данных.
В данном случае учебник говорит нам ровно наоборот:
Считайте, что содержимое главной страницы (список категорий и объявлений) получено от пользователя, поэтому его нужно соответствующим образом фильтровать
Сразу забивая в голову студента идею «источника данных», на который надо ориентироваться при экранировании. Простая мысль о том, что источник может со временем поменяться, или даже в самых супер-доверенных данных может оказаться нежелательный символ, автору курса в голову не приходит. В итоге у 80% студентов дыра в проекте, которая очень удачно заложена порядком заданий: в первом в тег
выводится статическое значение, а в дальнейшем — пользовательский ввод. Но про несчастную шапку никто уже, разумеется, не вспоминает. Не говоря уже о том, что сам по себе подход — после каждого изменения в коде бегать по шаблонам с лупой и искать, у какой переменной поменялся источник, не надо ли её собираться начать экранировать — выглядит на редкость дурацким.
Вот что стоило вместо подхода с «черным списком», да к тому же ещё и совершенно невнятным (а какие данные мы получили от пользователя? Из формы? А из БД — они ещё от пользователя или уже нет?), сразу использовать «белый»: экранируется вообще всё, только если явно не задано обратное — как это принято во всех современных шаблонизаторах?
Почти каждый студент задаёт вопрос — «А вот это значение надо экранировать? А это?». На что всегда хочется ответить в стиле поручика Ржевского из анекдота, который я здесь приводить не буду: «Выводим в HTML? Значит экранируем!» А что это за данные, откуда они взялись — нас не должно интересовать от слова «совсем».
Как правильно защищаться?
Корректно экранировать выводимые данные в зависимости от контекста.
Если это HTML, то обрабатываем через htmlspecialchars()
(с флагом ENT_QUOTES
). Если передаём данные в Яваскрипт — то через json_encode()
, если в URL query string — то urlencode()
/http_build_query()
— и так далее.
Причём иногда разные варианты экранирования сочетаются в довольно интересных комбинациях:
— именно так надо выводить аргумент функции на Яваскрипте в атрибуте HTML тега через РНР. Никакие символы, содержащиеся в $v
не поломают нам разметку или код — и при этом и HTML и Яваскрипт отработают штатно.
И самое главное — все эти правила надо применять безусловно, вне зависимости от каких-либо частностей или посторонних соображений.
Вместо заключения
Это правило кажется мне таким важным, что я даже решил выделить его в отдельный раздел. Все защитные механизмы должны применяться безусловно, на автомате. Без рассуждений вида «ну эти данные безопасные, их защищать не будем». Любые данные опасны, любые данные надо защищать. Причём это касается всех уязвимостей — как мы видели выше, с SQL инъекциями применяется тот же принцип безусловности защиты. Нас никак не должен волновать ни предполагаемый состав данных, ни источник их происхождения, ни любые другие соображения. Единственное что важно — пункт назначения. И — разумеется — обязательность форматирования. Также важно помнить, что форматирование должно производиться перед самым использованием данных, а не где-то сильно заранее. Если это SQL запрос — то прямо при выполнении. Если это HTML — то в момент ввода. И так далее.
Самое обидное, что в целом-то курс неплохой, в нём есть немало положительных моментов. А недостатки — у кого их нет? Они не составляют проблемы, если своевременно выявляются и устраняются. В конце концов, это нормальный путь развития любого проекта. Но почему-то именно с этим у Академии проблемы гораздо хуже, чем с контентом. Странное нежелание исправлять даже мелкие огрехи уже давно стало местным анекдотом. Например, на одном из учебных проектов есть нерабочий яваскрипт, который не даёт заливать файлы, даже если сам по себе не используется — его вызов надо просто убрать из кода страницы. В итоге каждый новый студент мучается, дебажит заведомо нерабочий код (который вообще не имеет никакого отношения к учебному заданию), и потом радостный приходит в чат сообщить о находке, а ему показывают все предыдущие сообщения на эту тему.
Или об эту же ошибку спотыкается уже тяжёлая артиллерия — кто-то из свежих «наставников». Привыкший к современным подходам к разработке и управлению проектами, он с налёту наивно предлагает — «да исправлять ничего не надо, просто дайте доступ к репозиторию, мы всё поправим, нужно будет только ПР принять». Казалось бы — вот оно, решение проблемы! Краудсорсинг, спасение утопающих руками самих утопающих. Если у авторов настолько плотный график по разработке новых курсов, что нет времени заниматься правками в уже существующих, то вот оно — идеальное решение! Но по какой-то причине все такие предложения забалтываются невнятными заверениями о том что всё сложно, доступ дать нельзя, но мы обязательно поправим.
Уязвимость с заливкой шелла является наиболее показательной в этом плане. Автору курса в чате Слака была наглядно продемонстрирована эта уязвимость, примерно так же, как здесь — с живыми примерами. Казалось бы — там правок в учебнике на 1 абзац, просто добавить информацию о том, что проверять расширение тоже обязательно. Но по какой-то причине это так и не было сделано.
В конце прошлого года, путем многомесячных напоминаний, мне удалось добиться невозможного. Какие-то ржавые колёса провернулись, бюрократическая машина попыхтела и неожиданно дала мне доступ к репозиторию с учебником по РНР. Радостный, я с энтузиазмом взялся за работу, тем более что буквально за пару дней до этого назад был ошарашен заявлениями студента, почерпнутыми им из учебника, причем из главы, в которую я никогда не заглядывал — там вроде и накосячить-то негде. Не желая затягивать процесс, я за сутки представил обновлённый вариант, постаравшись переработать материал так, чтобы он был более стройным, логичным, и не содержал фактических ошибок… В течение трёх месяцев мне приходилось постоянно напоминать о висящем PR, после чего он с некоторыми правками был принят, а ещё через два месяца по какой-то причине проект свернули, все правки откатили и доступ к репозиторию у меня отобрали. Я так до сих пор и не знаю, что это было.
Удивительно, что любые попытки что-то исправить уходят в «в молоко». Есть буквально считанные исключения из этого правила, но это просто капля в море. При том что чисто внешне с Q&A у Академии всё просто замечательно:
- под каждой главой учебника есть форма обратной связи, в которую можно написать замечания
- у гите Академии есть отдельный баг-трекер, в котором можно создавать issues
- есть есть многочисленные чаты в Слаке, как общие, так и по курсам
- есть «кураторы», которые тоже по идее должны реагировать на замечания и предложения
- есть даже специальная штатная единица «руководителя направления по работе с наставниками»
Но при этом единственная реакция, которую можно получить — это заверения о том, что все будет обязательно исправлено. Когда-нибудь. И работа этого самого «руководителя направления по работе с наставниками» заключается не в том, чтобы реагировать на замечания, а в том, чтобы гасить любые негативные эмоции. Вот эта работа в Академии поставлена очень хорошо. Огромное количество сотрудников занимаются только одним — следят за «позитивом». Типичная трёхходовка, «вот проблема, вот как её исправить», «спасибо, принято!», «эээ —, а вот мы снова наступили на эти же грабли, их что — не исправили?», которая затем превращается в чеырёх-, пятиходовку и так далее далее — утыкается в заверения о том что всё хорошо и не надо нервничать. Причём утешителей тоже можно понять: они разрываются между посторонним, в общем-то, человеком, искренне желающим помочь проекту, и сотрудником Академии, который не желает делать свою работу. И в итоге «руководитель направления по работе с наставниками» даже жаловался мне на нервные срывы, которые случаются у него на этой почве. Охотно верю. У меня бы тоже был нервный срыв.
Скажу честно — у меня нет подходящего объяснения такого отношения. Особенно учитывая серьёзность проблемы. Это всё-таки не вопрос, в каком порядке запятые расставлять. Но все объяснения, которые я смог придумать, не кажутся мне правдоподобными.
Можно попробовать списать такое отношение на личность автора курса. В конце концов, в служебном чате часто появляются запросы вида «Уважаемые наставники, нет ли среди вас экспертов по flutter (ansible/whatever) — нам нужен человек, чтобы сделать review курса», но никогда не было такого запроса по РНР. Отчасти это объяснимо — РНР считается таким простачком, и каждый считает себя экспертом в этом языке. А зачем проверять за экспертом? Но эта версия не кажется особо жизненной — ведь кроме автора курса должны же быть и другие сотрудники, которым небезразлично качество образования в Академии? Почему такой жестокий игнор почти любых обращений?
Была у меня версия, что курс по РНР — это такой никому не нужный сиротка, который уже не актуален, а продвигается чисто по инерции. Но меня очень горячо и искренне заверяли что это не так.
Версию жадности — когда принцип работы курсов строится на подходе «сделать один раз абы как и потом стричь денежки с лохов» я не рассматриваю, в такой цинизм я сам поверить не могу.
От самих «кураторов», разумеется, ответа я тоже не получил. Но если честно, то очень хочется узнать разгадку этой тайны.
В заключение я возьму на себя смелость дать несколько рекомендаций для администрации Академии. И в первую очередь — серьёзнее подходить к проблемам качества. Причём по-настоящему, не делая из Q&A карго-культа. И демонстрировать не показное, а реальное внимание к запросам и чаяниям наставников и студентов. Не заметать проблемы под ковёр, замалчивая их всеми силами. Освоить современные способы управления информацией — в частности, git и краудсорсинг. Больше задействовать инициативу и волонтёрство.
Телеграм канала у меня нет, так что прорекламировать статьёй нечего. Но если у вас есть вопросы, как по статье, так и по любым другим учебным материалам, а так же по безопасности веб-приложений в целом — обращайтесь, в комментариях или в личку — я постараюсь всем помочь и ответить. Также готов провести аудит РНР курсов на предмет адекватности, современности и безопасности.