Отчёт по кабинету: безопасность, режим ожидания, покрытие данных и сверка с типовым набором брокеров
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
|||||||
|
# Кабинет клиента: безопасность, связь без публичного адреса, покрытие данных
|
||||||
|
|
||||||
|
Отчёт на 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) — в коммит
|
||||||
|
не включён; в таких страницах бывают ключи, файл стоит удалить.
|
||||||
Reference in New Issue
Block a user