Вход, пароли и права

Забегаем вперёд. В прогоне курса кабинет «Кодиума» уже выложили на сервер в Москве (выкладку разберём в урок 5.5), 8 тестов из 8 были зелёные, вход открывался. При чтении кода нашлась одна строка: если секрета для подписи входа в настройках нет, программа берёт запасной, change-me-in-production. В .env на сервере секрета не было. Значит, пропуск владельца мог подписать любой, кто знает эту фразу, а она лежит в коде. Ни один тест этого не заметил. В этом уроке разбираем вход изнутри и ищем такие дыры у себя на компьютере.

Чему научишься в этом уроке:
• объяснять, как кабинет хранит пароли и помнит, кто вошёл;
• проверять пять защит входа: пароль, cookie, CSRF, лимит попыток, права;
• находить секреты «по умолчанию» в коде;
• проверять роли глазами, войдя под каждой.

Пароль: храним отпечаток, а не сам пароль

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

Отпечатки делает библиотека pwdlib алгоритмом Argon2. Это рекомендация документации FastAPI и памятки OWASP (открытого сообщества по безопасности сайтов). Argon2 нарочно медленный и требует памяти, поэтому подбирать пароли перебором долго и дорого. Для справки, минимальные настройки по OWASP: память 19 МиБ, 2 прохода, 1 поток. Старый bcrypt допустим, только если Argon2 недоступен, и он молча обрезает пароль после 72 байт.

Чтобы не спрашивать пароль на каждой странице, после входа кабинет выдаёт браузеру пропуск. Это cookie (маленькая запись, которую браузер хранит и отдаёт сайту при каждом запросе). В пропуске написано «это owner», и он подписан секретом SESSION_SECRET. Подпись похожа на печать на справке: подделать текст можно, а печать без печати нет.

Отсюда главное: кто знает секрет, тот может нарисовать пропуск «это owner» и войти владельцем без пароля. Поэтому секрет длинный, случайный, лежит только в .env на сервере. Ещё два флажка из задания: cookie ходит только по HTTPS (https_only) и same_site=lax. Lax значит: при обычном переходе по ссылке с чужого сайта cookie отправляется, а при отправке формы методом POST с чужого сайта нет. Поэтому всё, что меняет данные, делается только формой, и в ней есть ещё одна защита, CSRF-токен.

Почему не JWT, про который пишут в старых инструкциях? JWT (подписанный токен для программ) нужен, когда к серверу ходят мобильные приложения или другие программы. Для кабинета с HTML-формами подписанная cookie проще, и в П4 её хватает.

Ещё три защиты

  • CSRF-токен. Чужой сайт может незаметно отправить форму от имени вошедшего менеджера, например «сменить статус». Чтобы этого не случилось, в каждой форме кабинета есть скрытый секретный номер, который сервер выдаёт один на сессию, то есть на время входа. Форма без него получает отказ. Как номерок в гардеробе: пальто отдают только тому, у кого номерок.
  • Лимит попыток входа. Не больше 5 попыток в минуту с одного адреса, 6-я получает ответ 429 («слишком много запросов»). Перебирать пароли так очень долго. На сервере добавим ещё лимит в nginx.
  • Права и SQL с параметрами. Роль проверяется в каждом обработчике. Запросы к базе собираются с параметрами, а не склейкой строк, поэтому текст ' OR 1=1 в поиске останется просто текстом и не превратится в команду базе.

Всё это есть в задании прошлого урока, и на всё есть тесты. А теперь о том, на что теста не было.

Кейс: секрет по умолчанию

В коде стояла строка, обычная для примеров в интернете:

os.getenv("SESSION_SECRET", "change-me-in-production")

Она значит: «возьми секрет из .env, а если его нет, подставь запасной». Удобно, чтобы программа запустилась у разработчика. Опасно на сервере: при выкладке агент не записал SESSION_SECRET в .env, программа молча взяла запасной, и пропуска подписывались фразой из кода. Тесты прошли, вход работал, кабинет выглядел исправным.

Нашли это чтением кода и .env на сервере, тесты молчали. Задание агенту на исправление из прогона (пункт 2 про сервер, у себя его пропусти и вернись к нему после урока 5.5):

Серьёзная уязвимость: в /opt/crm-kodium/.env на сервере нет SESSION_SECRET, а в main.py стоит os.getenv("SESSION_SECRET", "change-me-in-production"). Значит, cookie входа сейчас подписываются известной строкой из кода — любой может подделать вход владельцем. Тесты этого не ловят.
Почини:
1. Никаких секретов по умолчанию в коде: если SESSION_SECRET или INGEST_KEY не заданы — приложение не запускается и пишет понятную ошибку (без значения секрета). Тест на это.
2. На сервере <адрес сервера> сгенерируй новый SESSION_SECRET (длинный случайный), добавь в .env (права 600), перезапусти службу. Все старые сессии станут недействительны — это нормально.
3. Найди в коде и конфигах остальные значения «по умолчанию» для секретов/паролей и убери их так же.
Покажи: тесты (pytest -q, полный итог), systemctl status crm-kodium, и что вход владельцем по-прежнему работает (curl с паролем из /root/crm-paroli.txt — сам пароль не печатай).

Агент работал 12,7 минуты: убрал запасные значения, добавил тест, сгенерировал новый секрет. Только после этого исправления вход владельцем проверили на сервере, и он работал. Но в том же задании агент удалил .env, об этом в 5.5. Теперь без SESSION_SECRET или INGEST_KEY программа не стартует и пишет понятную ошибку. Правило на будущее: у секрета не бывает значения по умолчанию. Нет секрета, значит программа не работает, и это лучше, чем работает с известным всем секретом.

Как проверить самому: в папке проекта выполни grep -rnE "getenv|environ" . --include=*.py --exclude-dir=.venv или попроси агента найти в коде все места, где читаются секреты (getenv, environ), и показать их списком «имя → есть ли запасное значение». Любое запасное значение у секрета, пароля или ключа убирать. Само значение секрета при этом не показывать, ни агенту в чат, ни в журнал.

Пароли сотрудников: в файл, не в чат

Первого владельца создаёт команда python manage.py create-owner <логин>. Пароль она спрашивает в консоли и в журнал не пишет. Вводишь его ты сам, агенту не диктуешь. На сервере при выкладке агент создаёт владельца и менеджера со случайными паролями и кладёт их в файл, доступный только администратору сервера (права 600, как .env в уроке 2.5). Пароли не печатаются ни в ответе, ни в журнале. Ты читаешь файл на сервере сам (команда в уроке 5.5), переносишь пароли в менеджер паролей и входишь.

Проверка ролей глазами

Тест «менеджер получает 403» хороший, но не заменяет взгляда. В прогоне оператор вошёл в кабинет под каждой ролью. Менеджер видит в меню только «Заявки»: фильтр по статусу, поиск по имени или телефону, таблицу заявок с кнопкой «Открыть». Владелец видит ещё «Пользователи», «Аналитика» и «Экспорт».

Перерисованный экран «Кабинета заявок» под менеджером: в адресной строке https://<твой поддомен>/, в тёмном меню «CRM Кодиум» и только «Заявки», справа «manager (manager)» и «Выход». Ниже фильтр «Статус», поле «Поиск: Имя или телефон», кнопка «Искать», таблица с колонками ID, Имя, Телефон, Курс, Источник, Статус («новая»), Создана (2026-10-10 13:27 МСК и т. д.) и кнопкой «Открыть». Телефоны тестовые.
Тот же экран под владельцем: в меню «Заявки», «Пользователи», «Аналитика», «Экспорт», справа «owner (owner)». Таблица заявок та же: №3 «Проверка ключа», №2 «Тест CRM Интеграции», №1 «Тестовая заявка», курс Python, источник «сайт» или «test», статус «новая», время 13:27, 13:25 и 13:00 МСК. Телефоны тестовые.

Дальше два действия руками. Под менеджером открой адрес страницы пользователей, набрав его в браузере: должен быть отказ. Под владельцем открой карточку заявки и смени статус, затем загляни в журнал: там должна появиться строка «кто, когда, было, стало». В прогоне при такой проверке нашлась ещё одна странность, со временем заявок. Её разберём в следующем уроке, вместе с сервером.

Если не получилось

  • В коде есть getenv с запасным значением для секрета → задание-исправление выше, пункт 1 и 3.
  • После удаления SESSION_SECRET программа всё равно запускается → запасное значение осталось где-то ещё; попроси показать все места чтения секретов.
  • Ошибка при старте печатает сам секрет → попроси агента писать только имя переменной, без значения.
  • Менеджер видит пункт «Пользователи» или открывает его по адресу → права проверяются не в каждом обработчике; назови адрес и попроси тест на него.
  • После смены секрета всех выкинуло из кабинета → так и должно быть, старые пропуска подписаны старым секретом; войди заново.
  • Локально вход проходит, но сразу выкидывает на /login → cookie настроена только для HTTPS, а у тебя http://127.0.0.1. Попроси агента брать флаг https_only из .env: у себя 0, на сервере всегда 1. Или проверь роли уже на сервере в уроке 5.5.

Контрольный вопрос. В коде: os.getenv(«SESSION_SECRET», «change-me-in-production»), в .env на сервере секрета нет. Чем это опасно?

АНичем, программа же запускается
БПропуска входа подписаны фразой из кода, и любой, кто её знает, может войти владельцем
ВПароли перестанут проверяться
ГСайт будет работать медленнее

Подсказка: Кто ещё может прочитать эту фразу?

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Контрольный вопрос. Ты удалил SESSION_SECRET из .env у себя и запускаешь кабинет. Какое поведение правильное после исправления?

АКабинет запускается с запасным секретом
БКабинет запускается без входа
ВКабинет не запускается и пишет понятную ошибку без значения секрета
ГКабинет сам придумывает секрет и печатает его в журнал

Подсказка: Что лучше: не работать или работать с известным секретом?

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Контрольный вопрос. Как кабинет должен хранить пароли сотрудников?

АОткрытым текстом в базе, чтобы владелец мог напомнить
БВ .env рядом с ключом приёма
ВВ cookie браузера
ГОтпечатком Argon2 через pwdlib

Подсказка: Вспомни слепок ключа у консьержа.

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Задание. МИССИЯ «Вход без дыр». 1. Попроси агента показать все места в коде, где читаются секреты и пароли, списком «имя → есть ли запасное значение». Если запасные есть, дай задание-исправление из урока (пункты 1 и 3; пункт про сервер пока пропусти, если кабинет ещё не выложен). 2. Сломай и почини: убери SESSION_SECRET из своего .env и запусти кабинет (командой из отчёта агента или попроси агента запустить и показать вывод). Он должен отказаться стартовать с понятной ошибкой, где нет значения секрета. Верни секрет, кабинет снова запускается. 3. Создай владельца командой create-owner, пароль введи сам. Через владельца создай менеджера. 4. Войди под каждой ролью. Под менеджером набери адрес страницы пользователей руками. Под владельцем смени статус заявки и найди строку в журнале. Принято, если: приложены список «секрет → запасное значение» после исправления, снимок отказа запуска без SESSION_SECRET (без значения секрета), снимки меню под менеджером и под владельцем, снимок отказа менеджеру на странице пользователей и строка журнала о смене статуса. Паролей и секретов на снимках нет.

Подсказка: Если кабинет без SESSION_SECRET запустился, ищи запасное значение не только в main.py.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Что дальше

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

Итог:
• пароли храним отпечатком Argon2 (pwdlib), вход помнит подписанная cookie;
• CSRF-токен, лимит 5 попыток в минуту, права в каждом обработчике, SQL с параметрами;
• у секрета нет значения по умолчанию: без него программа не стартует;
• роли проверяешь сам, войдя под каждой.

← Назад  ·  ↑ В начало урока  ·  ⌂ В начало курса  ·  Вперёд →

Школа Виктора Комлева