[Из песочницы] История небольшого исследования легаси-кода

habr.png

Хорошо, когда в команде есть кто-то более опытный, кто покажет что и как надо делать, какие грабли и за каким углом подстерегают, и где скачать лучшие чертежи велосипедов за 2007 год на DVD. Эта история о том, как желаемое было выдано за действительное, что получилось в результате, и как был преодолен кризис.

Это случилось в ту пору, когда я, имея весьма, как мне казалось, посредственный опыт в разработке, искал место, где можно из недоджуниора эволюционировать (или мутировать) хотя бы в уверенного джунироа. Неисповедимыми путями Господними такое место нашлось, в довесок к месту прилагался проект, и «олдскульный» программер, который за свою карьеру систем написал больше чем девок перепортил. «Отлично! Проект, а следовательно деньги на ЗП есть, наставник прилагается, живем!» — подумал я, но затем, как в описании к типичному хоррору, герои в темной тьме столкнулись с ужасным ужасом…
Обо всем по порядку:

1. Размер имеет значение


Разработку начали на самописном кем-то когда-то php-движке, для хранения данных использовали (тут вы можете подумать MySQL\PostgreSQL\SQLite\MongoDB\Что-то-там-еще-но-обязательно-с-суффуиксом DB-иначе-пацаны-не-поймут, а вот и не угадали) api-шлюз.

«Нафига, используя php, вы приделываете к нему еще какой-то api-шлюз, и храните на нем данные? Не проще ли работать с api напрямую из js-кода? Или использовать СУБД+PHP?» — спросит видавший виды читатель. И будет прав. Но в ту пору я, не видавший еще видов, так не думал, ну кто ж его знает, крутые пацаны так наверное делают, и «олдскульным» программерам виднее.

Как мне было далее разъяснено:

  1. Шлюз = безопасность, никто не войдет и не выйдет просто так
  2. Шлюз = защищенное хранение данных, просто так не залезешь, +бекапы
  3. Шлюз = скорость, работает быстро и без сбоев, проверено временем
  4. Авторитетная точка зрения «олдскульных» программистов гласит — ваш php дырявый весь, любое веб-приложение взломано по умолчанию, поэтому нечего данные хранить рядом с ним


Характерной особенностью api-шлюза являлось то, что json-данные передавались в get-запросе. Да-да, те самые милые сердцу json-объекты, подвергались url-кодированию, и клались в query string. И все бы хорошо, как вдруг однажды… длины get-запроса перестало хватать. Тупо не влезал туда url-кодированный json, каналья! «Олдскульный» программер, почесав затылок, спросил:
«Шо будем делать? Подрос наш json, а мы и не заметили…».
«Ну, эээ, может тогда в post их будем передавать?» — предложил я, так вроде правильней будет.
«Ок, передавайте в post».
Это был атас номер раз.

2. Тайм-энд-бекап-менеджмент


Чтобы прикрутить новую функциональность в проект, надо было реализовать
соответствующие CRUD-запросы на шлюзе, чем собственно и занимался наш «олдскульный» товарищ. Проблема заключалась в том, что занимался он этим раз в 3 дня, выдавая — «Готово, проверяй». Проверки временами показывали что работало не все, например получение списка — ок, добавление нового элемента — не ок. На исправление и доработку уходило еще какое-то время, после чего можно было выпускать функционал в массовый доступ. Предложение самому заняться реализацией запросов на шлюзе, потому что это как минимум быстрее, было отклонено, потому что «там сложно, ты не разберешься». Итогом такого подхода стало замыкание работы «на себя». Если, например, нужно было что-то массово исправить в БД, то, выбирая между 3-х дневным ожиданием и реализацией исправлений самому через запросы — я выбирал 2-й вариант. Заказчики ждать особо не любили, новые вводные прилетали стабильно. Одну из таких вводных, а именно массовое проставление пользователям некоего признака мне и поручили реализовать, времени на все про все — час, начальство ждало красивый отчет. Здесь нас поджидает атас номер два-с.

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

POST /api/users
{
	"email”:”ivanov@mail.ru”,
	"password”:”myEmailIsVeryBig”,
	"name_last”:”Иванов”,
	"name_first”:”Иван”,
	"name_middle”:”Иваныч”,
	"birth”:”01.01.1961”,
	//а вот тут следует вольноопределяемая часть, что считаем нужным - то и отдаем
	"living_at”:”ул.Сусаниа, д.3 к.4 кв.24”,
	"phone_num”:”+70000000000”
	
}


Та необязательная часть, которую передавали в запросах добавления\обновления — сохранялась и отдавалась в полном составе (о том как это было реализовано — расскажу ниже). Суть да дело, время на месте не стоит, надо бы и задачу решить — обновить пользователей, проставить им метки. Но не гонять же каждый раз всю структуру? Надо проверить! Протестировал на себе — передал в запросе на обновление только одно поле, проверил, поле появилось, остальные данные на месте. Дело за малым — зациклить и обновить остальных.

Скрипт тихонько пыхтел, принимая и отдавая данные, и вроде бы все шло хорошо… как вдруг — звонок. «Мы не видим ФИО пользователей в системе!» — сообщают с того конца провода. «Да ну нафиг! Нормально ж отрабатывало!» — по спине пробежал неприятный холодок. Дальнейшее расследование показало, что действительно, в строке ФИО значилось », хотя все остальные данные были на месте. Что делать в такой ситуации? Разворачивать бекап!

«Товарищ «олдскульный» программер, уи хэв э проблем хир! Нужно бекап! Когда последний актуальный сделан?» — спрашиваю.

«Э-э-э… Сейчас посмотрю…. Не, бакапа нету».

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

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

С тех пор бекапы стали сниматься каждый день.

3. Deep striking


Шатко-валко, но работа двигалась, проблемы решались, какие-то быстрее, какие-то медленнее, как вдруг… заказчики спохватились, что система лежит на не пойми чьих серверах, и за такое отношение к ПДн и организации мероприятий по ЗИ в ИСПДн их по головке не погладят. Надо переносить сервера к себе.

Почему изначально система не была передана? У руководства была одна страсть — централизация. Руководство мечтало о системе которая будет делать всё! Нужно тебе детёнка в школу пристроить? Заходишь в систему, в специальный кабинет, там подаешь заявление. Нужно тебе, скажем пиццу заказать — заходишь в систему, в другой специальный кабинет, подаешь заявление на пиццу. Может тебе общения с прекрасными дамами\кавалерами захотелось? К вашим услугам третий специальный кабинет — там тоже заявление подаешь.И так до бесконечности.

Преимущества — один логин и пароль на всё, данные надежно и безопасно хранятся на шлюзе. Даже бекапы есть. И, заметьте, никто у нас эту систему не отнимет! А даже если отнимет — что дальше? Все равно не разберутся в системе защиты от «олдскульных» программистов — там сложно всё.

VDS с системой выгрузили, отнесли к заказчикам, они ее развернули, все танцует и поет, красота!

И тут меня накрыло волной любопытства и некоторых подозрений.

Если наше веб-приложение дырявое, то где же данные? Неужели остались на других серверах? А если систему решат закрыть извне — то все рухнет?

Простая проверка показала, что данные, как и сами обработчики шлюза стояли на этом же сервере. И, нет, их не перенесли туда по причине передачи сервера, они там были всегда.
Теперь у меня в распоряжении была та самая секретная «олдскульная» разработка, которую я и принялся исследовать. Конечно, крутого реверс-инжинирнга в стиле статей журнала «Хакер», с ollydbg, смещениями, и прочими крутыми штуками не получилось, поэтому делюсь тем, что есть.
Собственно разработка была вполнена на python, остались только .pyc-файлы, которые легко декомпилировались обратно в читаемый код. Скажу честно, много времени, целых 25 минут, ушло на то, чтобы разобраться как это работает.

Итак, сложная система, разработанная «олдскульным» программером, в которой мало кто может разобраться, состоит из:

  1. Скрипта, обрабатываемого апачем, который собственно и получал запрос. Что делал данный скрипт? Открывал соединение на определенный порт localhost`а и передавал туда запрос со всеми его данными. Всё. Интересности идут дальше.
  2. Серверной части, обрабатывавшей запросы от скрипта. Логика его действий была достаточно интересна. Во-первых, в коде не было никаких манипуляций с данными, и запросов в БД, вместо этого вызывались функции БД на PL\SQL. Вся логика, проверки, и прочее, все было заложено в функции БД. 50% скрипта составлял словарь содержащий имя запроса, сопоставленную ему функцию, и имена параметров функции, которые должны были соответствовать данным, переданным в строке get-запроса. JSON-данные, если они были нужны, передавали как отдельный параметр. Особенностью организации серверной части явилось резервирование подключения при аутентификации пользователя. Если логин и пароль обнаружены в БД — генерировался ИД сессии, а экземпляр открытого подключения складывался в словарь (и убивался по таймауту в 10 минут, чтобы не убивался — был специальный метод на продление жизни сессии), ключом являлся ИД сессии, который в БД напрямую не хранился. Как же именно связан ИД сессии с данными пользователя? Ведь есть запрос на получение данных, в который ИД пользователя не передается? Он работает, а значит что-то тут не так.


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

Невероятным (Go to > Definition, спасибо PhpStorm за понимание PL\SQL), непостижимым разуму обывателя страданием Истинное Знание Утерянной Цивилизации Олдскульных Программистов было все же обретено. В общем — при подключении, в функции проверки данных аутентификации генерировалась временная таблица, в которой хранился ид пользователя.

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

  • DDoS с помощью массовой аутентификации (подключения резезвировались, и, следовательно, упирались в лимит соединений СУБД, что с учетом имеющейся возможности продления времени жизни сессии позволяло полностью забить память подключениями, и работа новых пользователей в системе станет невозможна);
  • отсутствие защиты от брутфорса (кол-во неудачных попыток входа не детектится, не хранится, не проверяется;
  • отсутствие контроля действий с сущностями (например, список документов, запрошенный пользователем, выдавался с учетом организации, к которой пользователь привязан, при этом, если знать ИД документа, то можно успешно выполнить запрос на обновление\удаление документа, а список пользователей, хорошо хоть без паролей, которые, кстати, хранились в БД в открытом виде, без хеширования, мог получить вообще кто угодно).


И еще одна серьезная проблема — не формализованная схема хранения данных. Как и обещал ранее — рассказываю о хранении «любых полей» из JSON. Нет, они не хранились как строка в таблице. Они разбивались на key-value пары и хранились в отдельной таблице. Например, для пользователей было 2 таблицы — users, и users_data (string key, string value) — где собственно и хранились данные. Итогом такого подхода стало увеличение времени при сложных выборках из БД.

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

Мораль


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

Но тем не менее выводы следующие:

  1. Если вам говорят «там сложно, ты не разберешься» — значит там полный атас
  2. Если давят авторитетом — значит что-то нечисто
  3. Доверяй, но проверяй — безопасность — не состояние, безопасность — процесс, притом непрерывный, посему лучше убедиться в соответствии декларируемых качеств действительности, чем потом узнать что все пользователь вдруг стали «Ивановыми Иванами Иванычами», а бакапов нету.

© Habrahabr.ru