Files
Bridge-and-Join-s/docs/lk/report-2026-09-17-security-and-data.md

16 KiB
Raw Permalink Blame History

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

Отчёт на 17.09.2026. Всё перечисленное выкачено на astramost и влито в main (коммит 2a7442c).

1. Как кабинет подключается к ядру

Настройки → «Личный кабинет» → «Кабинет клиента на своём сервере»:

  1. создать ключ доступа (на странице он показан замаскированным, целиком попадает только в архив);
  2. выбрать способ связи;
  3. скачать архив, распаковать на сервере кабинета, выполнить sudo ./install.sh;
  4. опубликовать через 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) — в коммит не включён; в таких страницах бывают ключи, файл стоит удалить.