Files
portal/docs/observer/notes/2026-07-18-finder-csp-inline-script-fix.md
T
Дмитрий 1514991ceb @
style(sales): Pint — порядок импортов в SalesClientsController

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 14:40:02 +03:00

75 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Поиск клиентов был мёртв с 15.07 — CSP резал встроенный скрипт
**Дата:** 18.07.2026 · **Где:** `моя/sales-finder` (служба `/opt/sales-finder`, `lk.liderra.ru/finder/`)
## Симптом (со слов владельца)
«Вкладки по отраслям пропали», «выбираю — не работает». На скриншоте: выбран раздел
(«Автотовары», потом «Медицина»), второй список не появляется, кнопка «Собрать список» серая.
## Корень
Вся логика страницы жила во **встроенном** `<script>` внутри HTML (плюс атрибут
`onsubmit="return confirm(...)"` у крестика удаления). 15.07.2026 в 10:46 UTC службу завели
за nginx `lk.liderra.ru` (`location ^~ /finder/`, `SF_BASE_PATH=/finder`). Там на уровне
server висит
```
script-src 'self' https://smartcaptcha.cloud.yandex.ru https://yastatic.net https://*.yandex.ru https://*.yandex.net
```
— без `'unsafe-inline'`, без nonce/hash. Внутри `location` своих `add_header` нет, значит
CSP наследуется целиком. Браузер молча **не исполняет** ни встроенный скрипт, ни `on*=`-атрибуты.
`style-src` при этом содержит `'unsafe-inline'` — поэтому страница выглядела целой, но была мёртвой.
**Подтверждение датой:** из 87 прогонов все `done`, последний создан **14.07.2026 10:24 UTC**
за день до правки nginx. После неё — ни одного нового сбора.
Второй, тихий дефект того же корня: подтверждение удаления не срабатывало → крестик ✕ удалял
список **молча с первого клика**.
Про «вкладки по отраслям»: их не было никогда. До 14.07 разделы 2ГИС показывались группами
`<optgroup>` в одном списке (коммит `87db3bc5`), 14.07 коммит `c864fbd0` заменил их каскадом
«раздел → ниша → глубже». Это отдельный вопрос UX, к поломке отношения не имеет.
## Что сделано
- JS вынесен в **`web/static/app.js`**, отдаётся маршрутом `GET /app.js`
(`application/javascript`, `Cache-Control: no-cache`). Файл со своего домена CSP пропускает
по `'self'` — защиту портала **не ослабляли**.
- Данные (дерево рубрик, карта «уже собрано», адрес просмотра) едут в **data-атрибутах**
`#cascade` (`data-catalog` / `data-done` / `data-viewurl`) — атрибуты CSP не касается.
Выбран этот способ, а не `<script type="application/json">`, чтобы не зависеть от трактовки
«неисполняемых» script-блоков браузерами.
- `onsubmit`-подтверждение заменено делегированным обработчиком в `app.js`.
## Проверка
- Тесты: **252 зелёных** (`pytest`), из них 13 новых/переписанных — сначала RED, потом GREEN.
Ключевые: нет `<script>` и `on*=` в HTML, данные читаются из атрибутов, `/app.js` отдаётся
как JS с `no-cache`.
- **Живой браузер под боевым CSP:** локальный двойник с точь-в-точь прод-заголовком
(Playwright): выбор «Медицина» → появился второй список, «Частные стоматологии» → третий,
`rubric=chastnye-stomatologii`, кнопка `disabled=false`, 85 зелёных городов,
встроенных скриптов 0, в консоли только 404 favicon.
- **На бою после выката:** страница 200, встроенных `<script>` 0, `on*=` 0,
`<script src="/finder/app.js">`, `/finder/app.js` → 200 `application/javascript` `no-cache`.
## Выкат
Прод не под git → перед копированием сверил `sha256`: боевые `ui.py`/`app.py` были **байт-в-байт
равны** `HEAD` (чужого на проде не было). Бэкап `/tmp/sf-backup-csp-20260718-113602.tgz` +
копии `/tmp/ui.py`, `/tmp/app.py`. Скопированы 3 файла, `chown www-data`,
`systemctl restart sales-finder` (11:37:08 UTC, стартовала чисто).
**Откат:** распаковать бэкап в `/opt/sales-finder/web` и перезапустить службу.
## Грабли на будущее
- 🪤 **Любая страница за `lk.liderra.ru` не имеет права на встроенный JS и `on*=`-атрибуты.**
Отдельная служба за прокси наследует CSP портала целиком.
- 🪤 **Проверка «служба жива + логов ошибок нет» ничего не доказывает для фронта:** поломка
случается в браузере, до сервера запрос не доходит. Смотреть глазами/браузером.
- 🔑 Дата последнего успешного действия в БД — хороший датчик: разрыв совпал с правкой nginx
с точностью до дня.