Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
16 KiB
Кабинет клиента: безопасность, связь без публичного адреса, покрытие данных
Отчёт на 17.09.2026. Всё перечисленное выкачено на astramost и влито в main (коммит 2a7442c).
1. Как кабинет подключается к ядру
Настройки → «Личный кабинет» → «Кабинет клиента на своём сервере»:
- создать ключ доступа (на странице он показан замаскированным, целиком попадает только в архив);
- выбрать способ связи;
- скачать архив, распаковать на сервере кабинета, выполнить
sudo ./install.sh; - опубликовать через nginx с сертификатом (пример в README архива).
Инструкция с шагами — «Инструкции» → «Личный кабинет» (/admin/help/lk).
Два способа связи:
| Способ | Когда | Что нужно |
|---|---|---|
Кабинет обращается к ядру (BJ_MODE=direct) |
у ядра есть адрес, доступный серверу кабинета | адрес ядра в настройках, только https |
Ядро подключается к кабинету (BJ_MODE=agent) |
у ядра нет адреса в интернете | адрес кабинета в настройках ядра; входящих портов не нужно |
Во втором способе кабинет копит запросы клиентов, ядро само дозванивается до кабинета по его
доменному имени (4 параллельных подключения), выполняет запрос тем же обработчиком /api/lk/*
и возвращает ответ. Канал в обе стороны закрыт ключом модуля; ядро выполняет только пути
/api/lk/…. Состояние канала видно в настройках: подключено ли, время последнего обмена,
число обслуженных запросов.
2. Проверка безопасности
Аудит нашёл 5 критичных, 12 важных и 9 мелких замечаний. Исправлено:
Критичные
- перебор пароля без ограничений → лимит по адресу посетителя (10) и по логину (8) на 15 минут, одинаковая задержка ответа; проверено вживую: блокировка на 8-й попытке;
- живой токен сотрудника лежал в куке, читаемой скриптами → все куки
HttpOnly; - CSRF отсутствовал,
/impersonateсрабатывал по ссылке → токен на всех формах, вход сотрудника в кабинет клиента только POST,SameSite=Strict; - куки без
Secure→ ставится всегда, кроме обращения на localhost; - сессии и паспортные данные клиентов лежали на диске ядра открытым текстом → в файле теперь отпечаток токена (sha256) и ссылка на учётную запись, персональных данных нет.
Важные
- выход гасит все сессии, включая сохранённую сессию сотрудника; возврат из кабинета клиента гасит сессию просмотра в ядре;
- карточка клиента читается из БД на каждый запрос: блокировка и снятие прав действуют сразу;
- абсолютный срок сессии 24 ч (скользящий — 12 ч), привязка к браузеру; привязка к сети — настройкой, по умолчанию выключена (мобильных клиентов иначе выбрасывает);
- вход сотрудника в кабинет клиента пишется в
lk.auditи поднимает оповещение; - заголовки CSP / X-Frame-Options / nosniff / Referrer-Policy / HSTS, запрет кэширования страниц;
- таймауты сервера кабинета, ограничение тела запроса 1 МБ, перехват паники;
- предупреждение в журнале, если адрес ядра задан по открытому http;
- ключ модуля и токен сессии: при отказе источника случайных чисел — ошибка, а не суррогат;
- ключ на странице настроек замаскирован.
Мелкие
- одинаковое время ответа для несуществующего логина (защита от перечисления логинов);
- пароль от 10 символов, буквы + цифра или знак;
- сообщения и ошибки больше не едут в адресе страницы (не попадают в журнал nginx);
- шрифты вшиты в программу: страница не обращается ни к одному стороннему сайту;
- экранирование
% _ [в выборках по номеру договора; /healthбез ключа отдаёт только{"ok":true}.
Заодно найдена и исправлена ошибка: текст «неверный логин или пароль» клиенту не показывался
вовсе (шаблон ждал поле Error, обработчик передавал Flash).
Осталось осознанно
- боевой ключ лежит внутри скачиваемого архива — иначе установка не одной командой. В README и на странице настроек стоит предупреждение удалить архив после установки. Вариант на будущее — одноразовый код привязки вместо ключа;
- список клиентов у сотрудника ищется через
GET /clients?search=…— ФИО попадает в журнал nginx сервера кабинета. Лечится настройкой nginx (не писать query) либо переводом поиска на POST; - файл сессий переписывается целиком при входе/выходе — при тысячах одновременных сессий стоит перейти на отложенную запись.
Что уже было сделано правильно: bcrypt, выборка данных строго по договору из серверной сессии
(подменой запроса чужие данные не получить), право сотрудника проверяется в ядре, сравнение ключа
за постоянное время, параметризованный SQL, html/template без небезопасных вставок, поручения
сверяются с фактическим остатком, кабинет ничего не хранит на диске.
Тесты
| Набор | Что проверяет | Результат |
|---|---|---|
go test ./... |
весь проект | проходит |
go test -race ./modules/lk ./internal/lkgateway |
гонки в очереди режима ожидания и сессиях | проходит |
lksecurity_test.go |
файл сессий без токена и ПДн, права 0600, привязка к устройству, абсолютный срок, только просмотр у сотрудника, API без ключа/с чужим ключом/без сессии, перебор, пароли, LIKE | 6 из 6 |
lkagent_test.go |
настоящий кабинет + настоящий цикл ядра; чужой путь через канал не выполняется | 2 из 2 |
modules/lk |
CSRF, заголовки, запрет GET /impersonate, режим ожидания, страницы |
проходит |
scripts/lk-sec-smoke.sh |
работающий кабинет снаружи: заголовки, куки, страницы без входа, формы без токена, обход каталога, большое тело, поддельные куки | 34 из 34 |
живые (-tags live, только чтение) |
история, цены, карточка бумаги, сверка с DEPO, покрытие данных | проходит |
Запуск проверки снаружи: scripts/lk-sec-smoke.sh https://адрес-кабинета.
3. Какие данные кабинет показывает сейчас
Выборка — 60 реальных договоров, операции и доходы за 2 года:
| Раздел | Источник | Данные есть |
|---|---|---|
| Портфель (остатки) | DEPO OD_RESTS |
60 из 60 клиентов, 143 позиции |
| Оценка по рынку | экспорт QUIK Securities+Params |
77 из 143 позиций (54 %) |
| Операции по счёту депо | DEPO U_OP_INV_* |
58 из 60, 1278 строк |
| Доходы и налог | DEPO U_OP_INF_INCOME, OD_NDFL |
26 из 60, 350 строк |
| Корпоративные действия | DEPO U_CA_ACTS |
2 из 60 |
| Готовые выписки | DEPO U_OP_INF_EXTRACT |
10 из 60, 29 документов |
Почему оценено только 54 % позиций — это не ошибка запроса:
- 31 позиция — иностранные бумаги (ISIN US/XS/GB/GG), на Мосбирже не торгуются;
- 35 позиций — внебиржевые выпуски классов
FONT_OUT/FONT_OUT2(в основном бумаги группы Гарант-Инвест), рыночной цены у них нет.
Предложение: для облигаций без рыночной цены показывать оценку по номиналу с пометкой «по номиналу», для иностранных — строку без оценки с пояснением. Решение о способе оценки — за депозитарием.
4. Что показывают кабинеты брокеров и что из этого нам уже доступно
В экспорте QUIK лежат данные, которые кабинет пока не использует. Код клиента QUIK сопоставляется
с договором напрямую: BR746 ↔ БР-746, BRIIS514 ↔ ИИС, суффикс OUT — внебиржевой раздел.
| Что обычно есть у брокеров | Есть ли у нас данные | Источник | Оценка работы |
|---|---|---|---|
| Денежные средства по валютам | да: рубли у 998 клиентов, юани у 18, золото у 6 | MoneyLimits |
малая |
| Средняя цена покупки, прибыль/убыток по позиции | да: заполнено у 2439 из 2528 позиций (96 %) | DepoLimits.AwgPositionPrice |
малая |
| Стоимость портфеля одной цифрой | да: у 1104 клиентов | Portfolio.PortfolioValue |
малая |
| Структура портфеля (акции/облигации/фонды, валюты, эмитенты) | да, считается из уже загруженного | позиции + цены + CFI | малая |
| Календарь выплат (купоны, оферты, погашения по своим бумагам) | да | DEPO OD_COUPONS, OD_OFFERS |
малая |
| Сделки за день | да: 1692 сделки, 87 клиентов | Trades |
малая |
| Активные заявки | да: 1323 заявки, 94 клиента | Orders |
малая |
| История сделок за период | частично: в экспорте только текущий день | Trades + накопление, как сделано для НПР |
средняя |
| График стоимости портфеля во времени | нет истории: нужно начать копить ежедневный срез | новая таблица срезов | средняя |
| Маржинальные показатели (НПР1/НПР2, УДС, плечо) | да | Portfolio, схема npr |
малая |
| Признак ИИС, статус квалифицированного инвестора | да | префикс договора, DEPO IS_QUAL |
малая |
| Налоги: удержанный НДФЛ | уже показываем | DEPO OD_NDFL |
— |
| Справки и отчёты | уже показываем + заявка на документ | DEPO, lk.doc_requests |
— |
| Доходность в процентах годовых | нет: нужны вводы/выводы денег за период | SPECTRE или накопление | большая |
| Ввод/вывод денег, пополнение | нет — это операции брокера, не депозитария | — | вне модуля |
| Торговля из кабинета | нет — для этого есть QUIK | — | вне модуля |
Рекомендуемый порядок: деньги → средняя цена и прибыль/убыток → структура портфеля → календарь выплат → сделки за день. Всё это — малые работы на уже включённых пакетах экспорта. Для денег и сделок нужно решение: показывать ли брокерские данные в кабинете депозитария (технически готово, вопрос в продукте и в том, какие клиенты это должны видеть).
5. На что посмотреть завтра
- настройки → «Личный кабинет»: новый блок связи, замаскированный ключ, кнопка архива;
- кабинет
http://…:8099: вход (теперь с сообщением об ошибке), «Документы», блок корпоративных действий на «Доходах»; - после выкатки все клиентские сессии сброшены (сменился формат хранения) — войти заново;
- пароли короче 10 символов завести больше нельзя; существующие учётные записи продолжают работать;
- в корне репозитория лежит старый
s.html(сохранённая страница настроек от 14.08) — в коммит не включён; в таких страницах бывают ключи, файл стоит удалить.