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

154 lines
16 KiB
Markdown

# Кабинет клиента: безопасность, связь без публичного адреса, покрытие данных
Отчёт на 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) — в коммит
не включён; в таких страницах бывают ключи, файл стоит удалить.