Compare commits
7
Commits
e31476ebab
...
ff4895b63b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff4895b63b | ||
|
|
4b73657e4c | ||
|
|
82acc84311 | ||
|
|
933ee79e3c | ||
|
|
b0c055f926 | ||
|
|
44e12db886 | ||
|
|
08e4788a35 |
@@ -106,7 +106,9 @@
|
||||
- **2026-08-24-time-tickers-estimates.md** — v0.0.64–0.0.65: тикер текущего файла на всех этапах, мгновенные оценки времени по файлам и суммарно.
|
||||
- **2026-08-24-folder-select-implemented.md** — v0.0.68: кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь).
|
||||
- **2026-08-24-folder-zip-not-lost.md** — v0.0.69: архивы из папки без документов не теряются (добавляются как есть); раскрытие zip с документами; склонение счётчика.
|
||||
- **2026-08-24-v073-security-fixes.md** — v0.0.73: 6 фиксов по ревью Соннета (SSRF, path traversal, слабый SID, proc_error, zip-бомба, self-XSS); 1 отклонено (cleanup), 5 отложено.
|
||||
- **2026-08-24-pull-retry-limit-counter.md** — v0.0.72: ретраи pull из ВМ, корректная ошибка лимита сессии, счётчик дедупа.
|
||||
- **2026-08-24-code-review-sonnet.md** — код-ревью Соннета: 15 багов (SSRF, path traversal, слабый SID, zip-бомба и др.), промпт в docs/code-review-sonnet.md.
|
||||
- **2026-08-24-upload-logic-schema.md** — подробная схема логики загрузки + найденные баги/несоответствия.
|
||||
- **2026-08-24-help-button-limits-text.md** — v0.0.70: заметная кнопка HELP (вместо «?») + пояснение статуса «пропущен» в ограничениях.
|
||||
- **2026-08-24-ui-locks-session-freeze.md** — v0.0.66: блокировки UI (выбор/удаление) на время работы, заморозка сессии + кнопка «Новая сессия».
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# v0.0.73 — фиксы по код-ревью Соннета (безопасность + явные баги) (2026-08-24)
|
||||
|
||||
_code-fixes. По ревью `History/sonnet/2026-08-24-code-review-sonnet.md` (промпт `docs/code-review-sonnet.md`)._
|
||||
_Критично перепроверены в коде; спорные пункты — отклонены/отложены (см. ниже)._
|
||||
|
||||
## Исправлено (6 фиксов)
|
||||
|
||||
1. **SSRF** (`api_bp.py::upload_refs`) — добавлена валидация `url.startswith(VM_UPLOAD_PREFIX)`;
|
||||
недоверенный URL пропускается. Константа `VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"`.
|
||||
2. **Path traversal / zip slip** (`api_bp.py`) — функция `_safe_name()`: нормализует слэши,
|
||||
отбрасывает `..` и абсолютные пути, сохраняя подпапки (`Подпапка/Акт.txt` → как есть,
|
||||
`../../evil.pdf` → ""). Применена в `upload` и `upload_refs`.
|
||||
Unit-тест 8 кейсов — все OK.
|
||||
3. **Слабый SID** (`session.py`) — `uuid.uuid4().hex[:12]` → `uuid.uuid4().hex` (128 бит).
|
||||
4. **Серверная ошибка не отображалась** — серверное событие `event: error` (конфликт с встроенным
|
||||
EventSource) → `event: proc_error`; добавлен клиентский `addEventListener('proc_error', …)`
|
||||
с показом сообщения.
|
||||
5. **ZIP-бомба: обход через поддельный `file_size`** (`extractor.py`) — добавлена проверка
|
||||
`total_uncompressed > MAX_UNCOMPRESSED` ПОСЛЕ `zf.read()` с `break` (ранний выход).
|
||||
6. **Self-XSS через имя файла** (`index.html`) — добавлена `esc()` и применена к `f.name`
|
||||
в `rr()` и `procRow()`.
|
||||
|
||||
## Отклонено (критично к Соннету)
|
||||
- **«cleanup(sid) после /download»** — НЕ сделано: ZIP и CSV скачиваются РАЗДЕЛЬНЫМИ запросами;
|
||||
удаление сессии после отдачи ZIP сломало бы скачивание CSV. Сессия и так чистится по TTL (30 мин).
|
||||
|
||||
## Отложено (требуют решения/риск)
|
||||
- idx-мисматч при `expand_zips` (фильтр расширений на бэке) — связано с фичей v0.0.69
|
||||
(zip без документов «как есть»), требует решения по поведению.
|
||||
- Отмена не проверяется в extract-фазе (.doc liberta 120с) — отдельный фикс.
|
||||
- LLM-таймаут тихо обнуляет чанк — логирование/статус.
|
||||
- Debug-эндпоинт `session_files` — ограничить/закрыть.
|
||||
- LLM prompt injection — задокументировать (класс риска, не фикс кода).
|
||||
|
||||
## Проверка
|
||||
- `py_compile` (app.py, api_bp.py, session.py, extractor.py) — OK; `node --check` — OK.
|
||||
- `_safe_name` unit-тест — 8/8 OK.
|
||||
- `create_app()` стартует (VERSION 0.0.73).
|
||||
- Версия 0.0.72 → 0.0.73.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Временный сбой сертификата gitea и push (2026-08-24)
|
||||
|
||||
_infra. Прецедент, чтобы не терять время в следующий раз._
|
||||
|
||||
## Симптом
|
||||
`git push origin master` к `gitea.services.ngcloud.ru` падал:
|
||||
```
|
||||
SSL: certificate subject name (*.ngcloud.ru) does not match target host name 'gitea.services.ngcloud.ru'
|
||||
```
|
||||
При этом в браузере `https://gitea.services.ngcloud.ru/Nail/drhider.git` открывался.
|
||||
|
||||
## Диагностика
|
||||
- `gitea.services.ngcloud.ru` → `194.31.9.41` (стабильно, и с машины, и с ВМ).
|
||||
- На `194.31.9.41:443` отдавался **wildcard `*.ngcloud.ru`** (SAN: `*.ngcloud.ru, ngcloud.ru`),
|
||||
который НЕ покрывает `gitea.services.ngcloud.ru` (два уровня: `services.ngcloud.ru`).
|
||||
- Внутренний ClusterIP `10.96.52.11` (gitea.mgmt.nubes.ru, hosts ВМ) недоступен ни с машины, ни с ВМ.
|
||||
- На ВМ git-настроек нет (нет sslVerify=false), `credential.helper=store`.
|
||||
|
||||
## Итог
|
||||
Проблема была **на стороне gitea/ngcloud.ru** — временно отдавался неверный (wildcard) сертификат.
|
||||
Через ~10-20 минут сертификат вернулся корректным (`CN = gitea.services.ngcloud.ru`,
|
||||
SAN `DNS:gitea.services.ngcloud.ru`) → `git push` прошёл (`0479afb..e31476e`).
|
||||
|
||||
## Урок
|
||||
- Если push к gitea падает по SSL «*.ngcloud.ru doesn't match» — это временный сбой сертификата
|
||||
на стороне gitea (не код). Проверить: `echo | openssl s_client -connect gitea.services.ngcloud.ru:443 -servername gitea.services.ngcloud.ru | openssl x509 -noout -subject -ext subjectAltName`.
|
||||
- Не менять git config (`sslVerify=false`) и не синкать на ВМ — подождать и повторить push.
|
||||
- Аналогичный сбой DNS был у `contracts.kube5s.ru` в 18:42 (см. `2026-08-24-pull-dns-transient.md`) —
|
||||
вероятно, общее инфраструктурное происшествие ngcloud.ru в этот день.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Код-ревью DrHider — ответ Соннета (2026-08-24)
|
||||
|
||||
_sonnet. Промпт: `docs/code-review-sonnet.md` (12 файлов кода, 8 вопросов)._
|
||||
_Ревью по v0.0.72. Ничего не исправлено — только задокументировано._
|
||||
|
||||
## 🔴 Критичные
|
||||
|
||||
| Баг | Файл | ~строка | Суть | Фикс |
|
||||
|---|---|---|---|---|
|
||||
| **SSRF** | `api_bp.py` `upload_refs` | ~93 | `url` из клиентского JSON без валидации → `httpx.stream("GET", url)` + `follow_redirects=True`. Доступ к metadata/kube-apiserver | валидировать URL-prefix == `VM_UPLOAD_URL` до запроса |
|
||||
| **Path traversal / zip slip** | `api_bp.py` + `builder.py` | ~88, ~55 | `name = ref.get("name")` без санитизации → `../../evil.md` в выходном ZIP | `name = os.path.basename(ref.get("name",""))` |
|
||||
|
||||
## 🟠 Средние
|
||||
|
||||
| Баг | Файл | ~строка | Суть |
|
||||
|---|---|---|---|
|
||||
| Серверная ошибка не отображается | `api_bp.py` + `index.html` | ~388, ~840 | сервер шлёт `event: error` — это встроенное имя EventSource; клиент не слушает кастомное → «SSE connection failed» вместо реального msg. Переименовать в `proc_error` |
|
||||
| idx-мисматч при `expand_zips` на бэке | `obfuscator.py` + `index.html` | ~90, sendIdx | фронт фильтрует zip по `.pdf/.doc/.docx/.txt/.md`, бэк расширяет ВСЕ файлы → число/порядок не совпадает → `sendIdx[idx]=undefined` |
|
||||
| Слабый SID (48 бит) | `session.py` | 85 | `uuid4().hex[:12]`; убрать `[:12]` → 128 бит |
|
||||
| ZIP-бомба: обход через поддельный `file_size` | `extractor.py` | ~240 | лимит проверяется по `info.file_size` (central dir, подделывается) ДО чтения; проверять ПОСЛЕ `zf.read()` |
|
||||
| Неатомарность pull при ошибке | `api_bp.py` `upload_refs` | ~115 | при исчерпании ретраев 502 БЕЗ `session_id`; частичная сессия-сирота до TTL |
|
||||
|
||||
## 🟡 Мелкие
|
||||
|
||||
| Баг | Файл | ~строка | Суть |
|
||||
|---|---|---|---|
|
||||
| Отмена не работает в extract-фазе | `obfuscator.py` | ~130 | в extract-цикле нет `cancel_event.is_set()`; .doc (liberta 120с) → отмена ждёт все извлечения |
|
||||
| TOCTOU: сессия между check и pause_ttl | `api_bp.py` | ~165-171 | `get_files` (lock снят) → `pause_ttl`; маловероятно |
|
||||
| `fileTimers` не заполняется | `index.html` | ~792 | мёртвый код, fallback всегда 0.0с |
|
||||
| Self-XSS: `f.name` в innerHTML | `index.html` | ~280, ~380 | имя файла без escaping → `textContent`/`escapeHtml()` |
|
||||
| ZIP с non-doc расширениями → мусор | `extractor.py` | ~257 | фильтровать расширения в `expand_zips` |
|
||||
| Сессия не чистится после `/download` | `api_bp.py` | ~430 | 500МБ висит 30 мин → OOM; `cleanup(sid)` |
|
||||
| LLM-таймаут тихо обнуляет чанк | `llm_client.py` + `scanner.py` | ~70, ~220 | `except → return []`, PII не найден без предупреждения |
|
||||
| Debug-эндпоинт `session_files` | `api_bp.py` | ~139 | список файлов любой сессии по SID без токена |
|
||||
| LLM prompt injection | `scanner.py` | ~200 | текст документа дословно в prompt → подавление NER |
|
||||
|
||||
## Подтверждено корректным (Соннет)
|
||||
- Различимость «сессия не найдена» vs «лимит» ✓
|
||||
- Гонок на `cancel_event`/`queue` нет (thread-safe) ✓
|
||||
- Утечек httpx/zipfile/EventSource/таймеров нет ✓
|
||||
- Пустой txt, битый pdf, 0 файлов/все сверх лимита — обрабатываются ✓
|
||||
|
||||
## Что уже исправлено ранее (до этого ревью)
|
||||
- Ретраи pull (v0.0.72) — Соннет отметил остаточную неатомарность.
|
||||
- Разделение статусов «пропущен (лимит)» / «не извлечён» (v0.0.71).
|
||||
- Счётчик дедупа (v0.0.72).
|
||||
|
||||
## TODO-связь
|
||||
- Открытый баг кнопки «Обфусцировать» при 0 файлов — `docs/WhatTODO.md` (не покрыт ревью Соннета).
|
||||
@@ -0,0 +1,32 @@
|
||||
# Массовое тестирование v0.0.72 на проде — 4 блока (2026-08-24)
|
||||
|
||||
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
|
||||
|
||||
## Блок 1 — выбор папки (всё ✓)
|
||||
- Рекурсия + относительный путь: `Подпапка/Акт1.txt`.
|
||||
- Вложенный ZIP с кириллицей: `архивы/док.zip` → `архивы/Отчёт/Справка_Сидорова.txt`, `архивы/Счёт_на_оплату.txt` (путь zip + путь внутри).
|
||||
- Дедуп: дубль `Договор1.txt` отсеян → «Добавлено из папки: 4 файла» (было бы 5).
|
||||
- Не-документ `.bin` (60 МБ) — пропущен.
|
||||
|
||||
## Блок 2 — полный цикл (всё ✓)
|
||||
- 4 файла: «Обработано 4 файлов: 9.9с, ИИ 9.7с, токены 2508».
|
||||
- CSV замен (152 Б): `+7 111. → +7_000_000_0001`, `Ивановым И.И. → Семёнов_0001`,
|
||||
`Петрова Анна → Николаев_0001`, `Сидорова → Семёнов_0002`.
|
||||
- ZIP (775 Б) с путями: `Договор1.md`, `Подпапка/Акт1.md`, `архивы/Отчёт/Справка_Сидорова.md`, `архивы/Счёт_на_оплату.md`.
|
||||
- Содержимое обезличено: «Договор с Семёнов_0001, тел +7_000_000_0001» и т.д.
|
||||
|
||||
## Блок 3 — прерывание (всё ✓)
|
||||
- 9 txt, прервано на 9-й секунде: «⏹ Остановлено. Сохранено 4 из 9 файлов + таблица замен (6.5с, токены 3183)».
|
||||
- Частичный ZIP — только 4 готовых: `файл_3.md, файл_4.md, файл_5.md, файл_7.md`.
|
||||
- CSV замен сохранён (1149 Б). Кнопки скачивания видны.
|
||||
|
||||
## Блок 4 — заморозка/UI (всё ✓)
|
||||
- После прерывания: `fi.disabled`, `ub.disabled`, «Новая сессия» видна (заморозка).
|
||||
- HELP открывает модалку (display:flex).
|
||||
- «Новая сессия»: список очищен, статус пуст, dl-кнопки скрыты.
|
||||
|
||||
## Найденный мелкий баг
|
||||
- После «Новой сессии» кнопка «Обфусцировать» активна при 0 файлов
|
||||
(`resetAll()`: `rr()` ставит `ub.disabled=cntMain===0` (true), затем `ub.disabled=false` перекрывает).
|
||||
Нажатие безопасно (`uploadFiles` → `if(sf.length===0) return`), но кнопка выглядит активной.
|
||||
Фикс — 1 строка (не перекрывать после `rr()`). Открыто, ждёт решения.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Тест v0.0.72 на проде — статусы, счётчик, полный цикл (2026-08-24)
|
||||
|
||||
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
|
||||
|
||||
## Сценарий
|
||||
Папка: битый PDF (scan1.pdf) + 3 txt + файл 51 МБ (Big.txt).
|
||||
|
||||
## Результаты (все ✓)
|
||||
1. **Счётчик дедупа (v0.0.72)** — папка с дублем [Договор.txt, Договор.txt, Акт.txt]
|
||||
→ «✅ Добавлено из папки: 3 файла» (дубль отсеян, раньше было бы 4).
|
||||
2. **Статус «не извлечён» (v0.0.71)** — во время обработки битый `scan1.pdf` в группе
|
||||
«✓ Обработанные (1)» со статусом «не извлечён» (раньше было «пропущен»).
|
||||
3. **Статус «пропущен (лимит)» (v0.0.71)** — `Big.txt 51.0 MB` в отдельной группе
|
||||
«⛔ Пропущены (сверх лимита)» (раньше попадал в «Ожидают обработки»).
|
||||
4. **Полный цикл** — «✅ Обработано 3 файлов: 5.6с, ИИ 5.4с» (3 txt), битый/большой исключены.
|
||||
5. **HELP + текст ограничений** (v0.0.70) — на месте.
|
||||
6. **Ретраи pull (v0.0.72)** — загрузка прошла штатно (код на бэке, косвенная проверка).
|
||||
|
||||
## Замечания
|
||||
- Статусы «не извлечён»/«пропущен (лимит)» видны ТОЛЬКО во время обработки
|
||||
(3-секционная таблица); после завершения `rr()` рисует обычную таблицу.
|
||||
- Страница приведена в чистое состояние («Новая сессия» → «Нет выбранных файлов»).
|
||||
+13
-1
@@ -2,4 +2,16 @@
|
||||
не надо ДОГАДОК !
|
||||
не надо самостоятельно что либо "УЛуЧШАТЬ" - никаких изменений рабочего кода без прямого разрешения
|
||||
есть сомнения - лучше остановись и спроси
|
||||
ВСЁ записывай в хистори ! что планировал что сделал в чём ошибся и тд
|
||||
ВСЁ записывай в хистори ! что планировал что сделал в чём ошибся и тд
|
||||
|
||||
---
|
||||
|
||||
## TODO (следующая правка кода)
|
||||
|
||||
1. **Кнопка «Обфусцировать» активна при 0 файлов после «Новой сессии»** (найдено 2026-08-24,
|
||||
массовое тестирование v0.0.72, `History/tests/2026-08-24-v072-many-tests.md`).
|
||||
Причина: `resetAll()` сначала вызывает `rr()` (ставит `ub.disabled = cntMain === 0` → true),
|
||||
затем `ub.disabled = false` перекрывает это. Нажатие безопасно (`uploadFiles` проверяет
|
||||
`sf.length === 0`), но кнопка выглядит активной при пустом списке.
|
||||
Фикс: в `resetAll()` не перекрывать `ub.disabled` после `rr()` (либо перенести `ub.disabled`
|
||||
установку до `rr()`). Бамп до v0.0.73 + редеплой.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Код-ревью DrHider — промпт для Соннета
|
||||
|
||||
Ты — строгий ревьюер. Проведи код-ревью сервиса DrHider (Flask + vanilla JS) —
|
||||
обфускация персональных данных в документах.
|
||||
|
||||
## СТРОГО просматривай ТОЛЬКО эти файлы (больше ничего не читай)
|
||||
|
||||
- `site/app.py`
|
||||
- `site/routes/api_bp.py`
|
||||
- `site/session.py`
|
||||
- `site/templates/index.html`
|
||||
- `drhider/__init__.py`
|
||||
- `drhider/builder.py`
|
||||
- `drhider/config.py`
|
||||
- `drhider/extractor.py`
|
||||
- `drhider/llm_client.py`
|
||||
- `drhider/obfuscator.py`
|
||||
- `drhider/replacer.py`
|
||||
- `drhider/scanner.py`
|
||||
|
||||
НЕ смотри: `History/`, `docs/`, `tests/`, `file_list.csv`, `files/`, `TMP/`, `TSTFILES/`.
|
||||
|
||||
## Контекст (кратко, не лезь в файлы)
|
||||
|
||||
- Загрузка файлов: браузер → PUT на ВМ-буфер (`contracts.kube5s.ru/drhider-upload/`),
|
||||
затем `POST /api/upload_refs` → Flask тянет файлы egress-GET (pull) в сессию.
|
||||
- Обработка: `GET /api/process_stream/<sid>` (SSE) → воркер-поток → `obfuscate_files()`
|
||||
→ извлечение текста → regex+LLM-сканирование → замена → ZIP + mapping.csv.
|
||||
- Лимиты: 50 МБ/файл, 500 МБ/сессия.
|
||||
- Есть мягкая отмена (кнопка «Прервать», `cancel_event`).
|
||||
|
||||
## Вопросы — ответь по каждому, с указанием файла и строки
|
||||
|
||||
1. **Корректность загрузки** (`api_bp.py::upload_refs`, `index.html::uploadFiles`):
|
||||
- атомарность pull при ошибке на середине списка файлов;
|
||||
- ретраи при DNS/сетевых сбоях — покрыты ли все transient-ошибки;
|
||||
- различимы ли «сессия не найдена» vs «превышен лимит сессии».
|
||||
|
||||
2. **SSE/потоки** (`api_bp.py::process_stream`, `obfuscator.py`):
|
||||
- корректна ли обработка disconnect клиента и остановка воркера;
|
||||
- нет ли гонок при `cancel_event`/`queue`/`pause_ttl`/`resume_ttl`;
|
||||
- TTL сессии: не умирает ли сессия во время долгой обработки.
|
||||
|
||||
3. **Соответствие индексов** фронт↔бэк (события `start/done/file_*` с `idx`):
|
||||
- `obfuscator.py::expand_zips` и `_dedupe_file_names` могут изменить число/порядок файлов
|
||||
после того, как фронт уже построил список — не ломается ли `sendIdx[d.idx]`?
|
||||
|
||||
4. **Статусы файлов** (`index.html`): консистентность «пропущен (лимит)» / «не извлечён» /
|
||||
«done» / «analyzed» / «pending»; нет ли путей, где статус зависает или перетирается.
|
||||
|
||||
5. **Безопасность** (`extractor.py`, `session.py`, `api_bp.py`):
|
||||
- path traversal через имена файлов (в т.ч. из ZIP);
|
||||
- zip-бомба (расширение, сжатие, суммарный размер);
|
||||
- инъекции/экранирование имён в HTML (`index.html` — вывод имён файлов).
|
||||
|
||||
6. **Утечки/ресурсы**: закрытие `httpx`-сессий, `zipfile`, `EventSource`/`XHR`, таймеры
|
||||
(`setInterval`) — всё ли очищается в `resetAll`/`finishProcUI`/`finally`.
|
||||
|
||||
7. **Краевые случаи**: пустой файл, файл без текста, битый PDF, дубли имён, отмена
|
||||
на каждом этапе, 0 файлов, все файлы сверх лимита.
|
||||
|
||||
8. **Что я мог пропустить** — любые реальные баги/риски, не перечисленные выше.
|
||||
|
||||
## Правила ответа
|
||||
|
||||
- БЕЗ воды. Только конкретика: файл, функция, строка, суть бага, серьёзность (🔴/🟠/🟡), фикс.
|
||||
- Не предлагай «улучшения» вне рамок ревью (рефакторинг, стиль) — только баги и риски.
|
||||
- Если тебе нужен контекст, которого нет выше (назначение функции, формат данных,
|
||||
ожидаемое поведение), — **задай уточняющий вопрос, не домысливай**.
|
||||
- В конце — итоговая таблица: баг | файл | строка | серьёзность | что делать.
|
||||
@@ -339,6 +339,9 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
|
||||
|
||||
inner_data = zf.read(info)
|
||||
total_uncompressed += len(inner_data)
|
||||
if total_uncompressed > MAX_UNCOMPRESSED:
|
||||
log.warning("ZIP uncompressed limit exceeded (mid-read): %s", fname)
|
||||
break
|
||||
|
||||
# Вложенные ZIP добавляем в очередь на повторную распаковку
|
||||
queue.append((name, inner_data, ""))
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
|
||||
sys.path.insert(0, _sys_path_root)
|
||||
|
||||
# Версия приложения (меняется при изменениях)
|
||||
VERSION = "0.0.72"
|
||||
VERSION = "0.0.73"
|
||||
|
||||
|
||||
def setup_logging():
|
||||
|
||||
+29
-6
@@ -34,6 +34,24 @@ log = logging.getLogger("routes.api_bp")
|
||||
PULL_RETRIES = 3
|
||||
PULL_RETRY_DELAY = 2 # секунды между попытками
|
||||
|
||||
# Доверенный префикс ВМ-буфера — валидация URL при pull (защита от SSRF)
|
||||
VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"
|
||||
|
||||
|
||||
def _safe_name(name: str) -> str:
|
||||
"""Санитизировать имя файла: защита от path traversal, сохраняя подпапки.
|
||||
|
||||
Запрещает '..' и абсолютные пути; нормализует слэши. Возвращает "" если
|
||||
имя пустое или небезопасное.
|
||||
"""
|
||||
if not name:
|
||||
return ""
|
||||
name = name.replace("\\", "/")
|
||||
parts = [p for p in name.split("/") if p and p != "."]
|
||||
if not parts or any(p == ".." for p in parts):
|
||||
return ""
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
def _disconnect_exceptions():
|
||||
"""Исключения, означающие отключение клиента SSE."""
|
||||
@@ -54,16 +72,17 @@ def upload():
|
||||
added = 0
|
||||
had_unnamed = False
|
||||
for f in uploaded:
|
||||
if not f.filename:
|
||||
name = _safe_name(f.filename)
|
||||
if not name:
|
||||
had_unnamed = True
|
||||
continue
|
||||
data = f.read()
|
||||
log.info("upload: sid=%s file=%r size=%d", sid, f.filename, len(data))
|
||||
log.info("upload: sid=%s file=%r size=%d", sid, name, len(data))
|
||||
if len(data) > MAX_FILE_BYTES:
|
||||
log.warning("upload: file exceeds %dMB, skipped sid=%s file=%r size=%d",
|
||||
MAX_FILE_BYTES // (1024 * 1024), sid, f.filename, len(data))
|
||||
MAX_FILE_BYTES // (1024 * 1024), sid, name, len(data))
|
||||
continue
|
||||
if not add_file(sid, f.filename, data):
|
||||
if not add_file(sid, name, data):
|
||||
log.warning("upload: session not found/limit, sid=%s file=%r", sid, f.filename)
|
||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||
added += 1
|
||||
@@ -96,10 +115,14 @@ def upload_refs():
|
||||
try:
|
||||
with httpx.Client(timeout=120, follow_redirects=True) as client:
|
||||
for ref in refs:
|
||||
name = ref.get("name")
|
||||
name = _safe_name(ref.get("name") or "")
|
||||
url = ref.get("url")
|
||||
if not name or not url:
|
||||
continue
|
||||
# SSRF-защита: тянуть можно ТОЛЬКО с доверенного ВМ-буфера
|
||||
if not url.startswith(VM_UPLOAD_PREFIX):
|
||||
log.warning("upload_refs: unsafe URL, skip sid=%s url=%r", sid, url)
|
||||
continue
|
||||
# Лимит на один файл (50 МБ): сверх лимита — пропускаем (не участвует)
|
||||
if (ref.get("size") or 0) > MAX_FILE_BYTES:
|
||||
log.warning("upload_refs: file exceeds %dMB, skip sid=%s file=%r size=%s",
|
||||
@@ -381,7 +404,7 @@ def process_stream(sid):
|
||||
_, msg = evt
|
||||
log.error("process_stream: error event sid=%s msg=%r", sid, msg)
|
||||
try:
|
||||
yield f"event: error\ndata: {json.dumps({'error': msg})}\n\n"
|
||||
yield f"event: proc_error\ndata: {json.dumps({'error': msg})}\n\n"
|
||||
except _disconnect_exceptions() as e:
|
||||
log.warning("process_stream: disconnect on error sid=%s err=%r", sid, e)
|
||||
return
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ def create_session() -> str:
|
||||
Returns:
|
||||
Уникальный идентификатор сессии (UUID).
|
||||
"""
|
||||
sid = uuid.uuid4().hex[:12]
|
||||
sid = uuid.uuid4().hex
|
||||
with _lock:
|
||||
_sessions[sid] = {
|
||||
"files": [],
|
||||
|
||||
@@ -280,6 +280,7 @@ function fmtSec(s) {
|
||||
// Эмпирическая оценка времени обработки файла: сек/МБ (ориентировочно, до старта)
|
||||
const EST_MB_SEC = 12;
|
||||
function estForFile(f) { return f ? Math.max(1, Math.round(f.size / 1048576 * EST_MB_SEC)) : 0; }
|
||||
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
function rr() {
|
||||
if (procPhase === 'processing') { renderProcTable(); return; }
|
||||
@@ -290,7 +291,7 @@ function rr() {
|
||||
const rowCls = over ? ' class="row-over"' : '';
|
||||
const stTxt = over ? '<span style="color:#c0392b;">🔥 не учитывается</span>'
|
||||
: '<span style="color:#7d3c98;">~' + fmtSec(estForFile(f)) + '</span>';
|
||||
return '<tr id="row-' + i + '"' + rowCls + '><td class="name-cell">' + f.name + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" id="st-' + i + '" style="font-size:12px;">' + stTxt + '</td><td><button class="remove-btn" onclick="rm(' + i + ')">✕</button></td></tr>';
|
||||
return '<tr id="row-' + i + '"' + rowCls + '><td class="name-cell">' + esc(f.name) + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" id="st-' + i + '" style="font-size:12px;">' + stTxt + '</td><td><button class="remove-btn" onclick="rm(' + i + ')">✕</button></td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
const overCount = sf.filter(f => overNames.has(f.name)).length;
|
||||
@@ -307,7 +308,7 @@ function procRow(i, stTxt) {
|
||||
const over = overNames.has(f.name);
|
||||
const cls = (procState[i] && procState[i].st === 'current') ? ' class="row-current"'
|
||||
: (over ? ' class="row-over"' : '');
|
||||
return '<tr' + cls + '><td class="name-cell">' + f.name + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" style="font-size:12px;">' + stTxt + '</td><td></td></tr>';
|
||||
return '<tr' + cls + '><td class="name-cell">' + esc(f.name) + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" style="font-size:12px;">' + stTxt + '</td><td></td></tr>';
|
||||
}
|
||||
|
||||
function renderProcTable() {
|
||||
@@ -936,6 +937,14 @@ async function uploadFiles() {
|
||||
document.getElementById('newSessionBtn').style.display = 'inline-block';
|
||||
resolve();
|
||||
});
|
||||
activeES.addEventListener('proc_error', function(e) {
|
||||
const d = JSON.parse(e.data);
|
||||
finishProcUI();
|
||||
setBusy(false);
|
||||
st.className = 'status error';
|
||||
st.textContent = 'Ошибка обработки: ' + (d.error || 'неизвестная ошибка');
|
||||
resolve();
|
||||
});
|
||||
activeES.onerror = function() {
|
||||
finishProcUI();
|
||||
reject(new Error('SSE connection failed'));
|
||||
|
||||
Reference in New Issue
Block a user