Пост о том, как в нашу компанию пришла безопасность. История показывает наши первые шаги и что нас заставило их сделать.
Утечка: утро, которое всё изменило
Больше восьми лет я работал backend‑разработчиком. Мы создавали веб‑приложения для автоматизации логистики и закупок.
Команда росла, процессы крепли. Всё было правильно и красиво: CI/CD, код‑ревью, споры о чистоте архитектуры и идеальном нейминге. Мир был прост, предсказуем и казалось, что так будет всегда. Но однажды утром всё изменилось.
Я проснулся от назойливо вибрирующего телефона. Экран мигал именем ПМа.
Полусонный, я взял трубку, ожидая услышать что‑то вроде «деплой подвис» или «у клиента кнопка не нажимается».
Но голос на другом конце был сухим и непривычно резким:
— У нас утечка.
— Что значит — утечка?
— База клиента. Вся. В открытом доступе.
В голове щёлкнуло что‑то очень холодное.
Через двадцать минут вся команда была на связи. Мониторы светились логами, кофе пах горечью, а в чате висел единственный вопрос: Как это произошло?
Все взялись за логи...
Погружение в хаос: поиск причины
О, да! Мы те самые разработчики, которые не озаботились нормальным аудитом действий пользователей. Из всей телеметрии у нас остались только замеры скорости запросов — технический мусор, нужный для оценки производительности. Ни идентификаторов, ни истории действий. Только тайминги и местами тела запросов.
Несколько часов курения этих жалких обрывков — и, наконец, зацепка. Какой-то пользователь, ничего особенного: менял фон в чате. Только вот запросы у него были... странные.
GET /chat/background?img=moon
GET /chat/background?img=test_1 or 1=1 --
GET /chat/background?img=moon; SELECT table_name FROM information_schema.tables WHERE table_schema='public' --
После этого — аккуратное, методичное выкачивание всей базы.
Мы открыли код. Нужная ручка — получение фонового изображения. И перед глазами — строки, одни из тех, что заставляют сердце биться где‑то в горле:
const imageName = req.query.img; // Пользовательский ввод
const query = SELECT * FROM backgrounds WHERE name = ${imageName};
SQL‑инъекция. Прямая, как выстрел в висок.
Дальше всё стало по‑детски непредсказуемо. GitLab. История коммитов. Код написал джун. Отревьюил сеньор. Десять строк. Пропустить невозможно. Но — пропустили. И код тихо ушёл в прод с очередным релизом.
Мы решили что, это был не косяк пары человек, а системный сбой: в процессах, культуре, самоуверенности. Безопасность мы считали «чужой задачей». И вот теперь мы стояли по колено в обломках собственной уверенности, пытаясь понять, где именно наш «идеальный мир» дал трещину.
Видеть систему: построение DFD
Сначала мы просто сидели в тишине. Мониторы переливались графиками, а в голове стучала одна мысль: «Как так‑то?!!»
Казалось, что такого не может быть. Не у нас. Не в проекте, где всё под ревью, где пайплайны проходят автоматическую проверку, где даже eslint заставляет писать точку с запятой.
Но факт был очевиден — база ушла. Не сбоем, не случайностью, не ошибкой бэкапа. Её вынесли. Спокойно. Через API.
На постмортем мы начали копать с конца — от момента утечки. Кто запросил, какие endpoint'ы дергались, в каком порядке, с каким user‑agent. Но чем глубже лезли, тем яснее становилось: у нас нет картины мира. Нет схемы данных. Нет карты потоков. Нет даже простого понимания, где пользовательские данные пересекаются с прямыми SQL‑запросами. Мы знали код, но не знали, как он живёт.
К вечеру стало ясно: чтобы понять, где произошёл провал, нужно увидеть всю систему целиком. Не глазами разработчика, а глазами злоумышленника. Мы открыли Miro и начали рисовать.
Сначала — привычно: модули, стрелочки, API‑шлюзы. Потом добавили базу данных, сервис авторизации, хранилище логов. А потом — потоки данных. Кто с кем говорит, кто получает, кто отдаёт, кто вообще имеет право видеть то, что хранится в public.users.
Так родилась наша первая DFD — Data Flow Diagram, но, честно говоря, выглядела она как карта метро, нарисованная на салфетке. Зато впервые за долгие годы мы увидели, как течёт информация.
Чем дальше рисовали, тем страшнее становилось. Почти все пользовательские данные проходили через пару «удобных» обёрток, где параметры просто подставлялись в строку SQL‑запроса. «Технический долг», как мы это называли. Долг, который теперь требовал процентов.
Мы начали помечать узлы цветами:
красный — где возможен ввод пользователя,
желтый — где нет валидации,
серый — где просто не хватает данных, чтоб что‑то понять.
Через пару часов на схеме было больше красного чем, серого.
Постепенно приходило осознание: всё это время мы строили не систему — лабиринт. Снаружи он выглядел аккуратно, красиво, даже по инженерному изящно. Но внутри — узкие коридоры, где каждый запрос мог обернуться катастрофой.
Когда DFD была готова, мы впервые смогли задать правильный вопрос: не «где ошибка?», а «где у нас может быть следующая?»
Анализ угроз: STRIDE
Мы начали двигаться от конкретного инцидента к модели угроз. Каждая стрелка на схеме — потенциальный вектор атаки. Каждый компонент — цель. Так началось настоящее расследование. Не просто багфикс, а разбор преступления по методике.
В тот вечер Miro пахнул маркерами сильнее кофе. На экране — наша тол��ко что нарисованная DFD, внизу — список подозрительных запросов, в правом углу — глаза ПМа, которые уже не смотрели весело, как раньше.
Мы поняли: может быть, код написан красиво, но злоумышленник не интересуется красотой. Он идёт по потоку данных — и мы должны научиться предугадывать этот путь.
«Надо прогнать DFD через STRIDE», — сказал один сеньор (возможно это единственное что, он нагуглил на тот момент).
Мы взяли каждый узел на DFD (фронт, API‑шлюз, auth‑сервис, background‑endpoint, база) и прогоняли его по‑шагам:
Могут ли нас подделать (Spoofing)?
Могут ли изменить данные (Tampering)?
Может ли кто‑то отрицать свои действия (Repudiation)?
Что может утечь (Information disclosure)?
Как нам ударить чтоб система ушла в отказ (DoS)?
Можно ли поднять привилегии (Elevation of privilege)?
Это не было академическим упражнением — каждое «да» превращали в запись, пример и контрмеру. Ниже — выдержки из наших заметок по трём ключевым точкам.
Endpoint /chat/background (того самого фона):
Spoofing: низкий — авторизация на уровне сессии есть, но токены долго живут.
Контрмера: проверить TTL токенов, ужесточить ротацию API‑ключей.
Tampering: высокий — параметры подставляются в SQL (см. инцидент).
Контрмера: параметризованные запросы/ORM, строгая валидация входа.
Repudiation: средний — мало логов действий пользователей.
Контрмера: запросы логировать с user‑id и trace‑id.
Information disclosure: критический — через инъекцию можно читать структуру БД.
Контрмера: минимальные права у БД, опция бинарных ролей, запрет выдачи схемы наружу.
DoS: средний — endpoint может запускать тяжёлые запросы.
Контрмера: rate‑limiting, лимиты по времени выполнения.
Elevation of privilege: возможен при комбинировании инъекции и уязвимости в auth.
Контрмера: проверка прав на уровне БД и сервиса.
Auth сервис:
Spoofing: возможен через компрометацию токенов.
Контрмера: короткий TTL, refresh с проверкой IP/device fingerprint.
Repudiation: высок — операции без подписи/логов.
Контрмера: хранить immutable audit‑log.
База данных:
Information disclosure: критический — если у приложения права широкие, утечка — вопрос времени.
Контрмера: least‑privilege для connection user, database roles, read‑only реплики для аналитики.
STRIDE дал нам длинный список «что может пойти не так». Но у нас не было ответа, что чинить в первую очередь и... гуглеж дал нам DREAD.
Приоритизация: DREAD
Мы оценивали каждый риск по пяти параметрам (1–10): Damage (D), Reproducibility (R), Exploitability (E), Affected users (A), Discoverability (D).
Формула:
Пример для SQL-инъекции в /chat/background:
Damage = 10
Reproducibility = 10
Exploitability = 9
Affected users = 10
Discoverability = 8
Сумма: .
Средняя:.
Ниже — наша приоритизация (усечённо):
Угроза |
D |
R |
E |
A |
D |
Score |
Приоритет |
SQL‑инъекция в /chat/background |
10 |
10 |
9 |
10 |
8 |
9.4 |
Критично — fix now |
Широкие DB‑привилегии у приложения |
9 |
7 |
6 |
10 |
7 |
7.8 |
Высокий |
Отсутствие audit‑логов |
7 |
6 |
5 |
8 |
8 |
6.8 |
Средний |
Незафиксированные timeout/limit в тяжёлых эндпойнтах |
6 |
6 |
7 |
6 |
4 |
5.8 |
Низкий \ Средний |
Эти числа — не канонические истины, а инструмент: они позволили управлять отвлечённым вниманием команды и финансами менеджмента. Когда риск в 9.4 — это не «красивый баг», а срочный инцидент.
Брейншторм превратился в план действий. Мы делали маленькие, конкретные шаги:
Мгновенный патч — заменить уязвимую ручку на параметризованный запрос и закоммитить в hotfix‑ветку. Это уменьшило эксплойтуемость инъекции.
Ограничение привилегий — поменяли DB‑user, отобрав права на information_schema.
Логирование и трассировка — добавили trace‑id в каждый запрос и начали логировать user‑id + action.
Rate limits — поставили простые лимиты на тяжёлые endpoints, чтобы закрыть возможный DoS‑вектор.
План по рефакторингу — список endpoint'ов, где есть string-concat SQL, и сроки переработки.
Каждый патч сопровождался причинами и ссылками на соответствующие узлы DFD — так мы учили друг друга думать не «починил и забыл», а «почему это было уязвимо на архитектурном уровне».
Роль менеджмента и коммуникация
Мы сразу договорились: ничего не скрываем от клиента (если потребуется — уведомляем). Прозрачность давала нам пространство для действий и снижала риск «паники по телефону». Менеджмент помогал приоритизировать исходя из влияния на бизнес, а не только технического любопытства.
Итог ночного этапа
К рассвету у нас был:
исправленный критический endpoint в hotfix;
план по уменьшению привилегий БД;
карта уязвимых мест с пометками «кто за что отвечает»;
чёткая очередь задач по приоритету DREAD.
Мы выиграли время. Но мы знали, что ручные патчи долго не прокатят. Человеческий глаз устаёт, схемы быстро устаревают, а проект живёт быстро. Нужно было думать о том, как превратить эти проверки в рутину — чтобы следующий инцидент не начинался с вибрации телефона.
Следующие дни после ночного брейнштормa напоминали хронологию судебного следствия.
Каждый патч, каждый фикc — как улика, каждая диаграмма DFD — карта преступления.
Но мы понимали: вручную поддерживать такой контроль невозможно.
Слишком много узлов, слишком много точек входа, слишком высокая вероятность человеческой ошибки.
Решение пришло одно: автоматизация. Если код проверяется линтерами и тестами в CI/CD — значит, проверять можно и безопасность.
Semgrep — наш новый глаз
Мы выбрали Semgrep — дешево и сердито. Он ищет опасные паттерны в коде ещё на этапе merge request и не даёт уязвимому коду попасть в прод.
Первое, что мы сделали:
Определили самые критичные паттерны, которые привели к утечке: SQL-инъекции через string-concat в endpoint’ах.
Написали первое правило для Semgrep.
rules:
- id: unsafe-sql-query
patterns:
- pattern: |
const $VAR = $REQ.query.$FIELD
- pattern-either:
- pattern: |
`SELECT * FROM $TABLE WHERE name = ${$VAR}`
- pattern: |
"SELECT * FROM " + $TABLE + " WHERE name = " + $VAR
message: "Потенциальная SQL-инъекция. Используйте параметризованный запрос."
languages: [javascript, typescript]
severity: ERROR
Подключили Semgrep в CI/CD: теперь merge request блокировался, если правило срабатывало.
Помню, как мы сидели до полуночи:
один разветвлённый лог, четыре монитора, пустые кружки кофе — и каждая новая строчка правила Semgrep вызывала короткий радостный крик.
— Сработало!
— Фиксим ещё endpoint /orders!
Каждое новое правило — это маленькая победа над хаосом. Каждый патч — маленькая страховка от будущей катастрофы.
28 дней спустя
Semgrep стал обязательным шагом в CI/CD — вместе с unit‑тестами и ESLint.
Merge request теперь не только проверяли на стиль и тесты, но и на потенциальные уязвимости.
Команда начала писать правила сама — никто больше не ждал инструкций или коллапса.
DFD и STRIDE превратились из бумажных схем в живые инструменты: каждая новая функция обсуждалась через них и сразу проверялась Semgrep.
Безопасность перестала быть чем‑то внешним, она стала частью культуры разработки.
Построение схем и анализ угроз перенесли в Threat dragon сделав их живыми.
Таким образом маленькая SQL инъекция изменила целый процесс разработки и дала старт к дальнейшему развитию.
Комментарии (6)

Avangardio
23.10.2025 13:39Давно не писал комментарии, но тут прямо не очень стало по мере прочтения. Столько умных терминов, формул и анализов, в то время, когда в команде, в которой есть, так называемые, «сеньоры», произошла инъекция, когда уже несколько лет всё комьюнити сошлось на параметрах, просто поражает…

4yk Автор
23.10.2025 13:39Да, все верно. Но умные слова и формулы появились после инцидента, до этого акценты были на ином. Возможно, сеньор был перегружен или просто отправил на вере в чудо. Мы стараемся не вести охоту на ведьм, а адаптировать систему, чтоб проблема в дальнейшем не повторилась.

Wolfdp
23.10.2025 13:39На хабре когда-то проскакивала статья, что если вы не тестируете безопасность как нечто само-собой разумеющееся, то где-то так оно потом и выходит. Т.е. мы привыкли закладывать в задачу разработку, тестирование, написание документации, ревью, но вот проверку секрьюрити -- это что-то на фентезийном. А потом звонки и холодный пот...
P.S. мне казалось sql-иньекции это что-то на древнегреческом, и все давным-давно работают через параметры, которые априори не позволяют влепить исполняемый код в запрос.

4yk Автор
23.10.2025 13:39Да, я тоже считал древностью, а потом наткнулись. И эта оказалась одной из самых простых. Дальше мы нашли более жуткие вещи.
В погоне за скоростью ответа в системе отчетности, где пользователь через конструктор может построить необходимый ему отчет, приходилось отказываться от орм, потому что запросы генерились, мягко сказать, не оптимальные. Получилось что, можно через пару ручек выстроить полноценный доступ до всей бд. И sast такое почти не отслеживает.
А про безопасность по умолчанию- это шишка, но чужие шишки не болят, пока похожие не появятся.
А еще, когда у тебя в проект толпа сеньоров, которые спорят о лучших практиках, архитектуре и других высоких материях в купе с борьбой за скорость релиза. Как-то простые базовые вещи уходят на второй план и молча лежат пока не выстрелят. Самое страшное, что очень часто подобные штуки лежат в сторонних пятизвездочных пакетах, и ты о них даже не догадываешься.
P.S. в оправдание могу добавить, что прежде чем публиковать, пришлось дать истории отлежаться длительное время. Иначе можно получить незапланированные пентест, а это не сильно хочется)))
Steppi91
Дааа, жестко вас наказали, ну главное что вы поняли свои ошибки, а значит защищены от того чтобы их повторить. Но вот что странно, у вас тестирование не проводится что ли?
4yk Автор
Если речь про qa, то проводится. Но они сосредоточены на функционале и подобные вещи обычно не проверяют.
Если речь про пентест -проводится, но это истории с большим интервалом. Вот в такой интервал мы и попали.