v0.0.56: КРИТИЧНО — mapping.csv (ключ расшифровки) убран из выходного ZIP, CSV только отдельно
Deploy drhider / validate (push) Canceled after 0s

This commit is contained in:
“Naeel”
2026-08-20 09:38:28 +03:00
parent dfa325a5c5
commit 670f533f48
4 changed files with 36 additions and 20 deletions
+24
View File
@@ -0,0 +1,24 @@
# 2026-08-20 — КРИТИЧНО: mapping.csv (ключ расшифровки) убран из выходного ZIP (v0.0.56)
## Проблема (утечка секретных данных)
Выходной ZIP содержал `mapping.csv` — таблицу соответствия «оригинал → замена»,
т.е. ключ расшифровки (реальные ФИО/телефоны/ИНН ↔ токены). Если ZIP передать
третьему лицу — тот получал и обфусцированные файлы, и таблицу расшифровки.
CSV должен храниться/скачиваться ОТДЕЛЬНО от обфусцированных файлов.
## Фикс (drhider/builder.py `build_zip`)
- Убрана запись `mapping.csv` в архив. Теперь в ZIP — ТОЛЬКО обфусцированные файлы.
- Параметр `mapping_csv` сохранён в сигнатуре (для совместимости вызовов), но в
архив не пишется.
- CSV продолжает генерироваться (`build_mapping_csv`) и отдаваться отдельно
через `/api/csv/<sid>` (кнопка «Скачать CSV»), как написано на странице.
## Тесты (tests/test_builder.py)
- Блок «ZIP с mapping.csv» переписан: теперь проверяет, что mapping.csv НЕ в архиве.
- Интеграционный блок: ZIP содержит только doc.md, CSV проверяется отдельной строкой.
- Все 106 тестов OK.
## Проверка
- python3 tests/test_builder.py — 20/20 OK (включая новый сценарий «mapping не в zip»).
- Остальные тесты — без изменений поведения, OK.
- VERSION 0.0.56.
+5 -11
View File
@@ -20,15 +20,16 @@ from .config import ENTITY_PATTERNS, COMPANY_PATTERN
def build_zip(files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes:
"""Собрать ZIP-архив с обфусцированными файлами.
В архив добавляются:
- Все обфусцированные файлы (с оригинальными именами)
- mapping.csv — таблица соответствия оригинал→замена (если не пустая)
В архив добавляются ТОЛЬКО обфусцированные файлы (с оригинальными именами).
mapping.csv в архив НЕ кладётся — это таблица соответствия оригинал→замена,
т.е. ключ расшифровки (секретные данные). Он хранится/скачивается отдельно
(/api/csv), чтобы нельзя было расшифровать архив, получив один ZIP.
Имена файлов в архиве — UTF-8 (бит 11 в flag_bits).
Args:
files: [(filename, content_bytes), ...]
mapping_csv: Строка CSV с таблицей замен (опционально)
mapping_csv: Строка CSV с таблицей замен (опционально, НЕ добавляется в архив)
Returns:
Бинарное содержимое ZIP-архива
@@ -63,13 +64,6 @@ def build_zip(files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes:
info.flag_bits |= 0x800 # Флаг: имя файла в UTF-8
zf.writestr(info, content)
# Добавляем mapping.csv с BOM (для корректного открытия в Excel)
if mapping_csv:
zf.writestr(
"mapping.csv",
'\ufeff'.encode('utf-8') + mapping_csv.encode('utf-8'),
)
return buf.getvalue()
+1 -1
View File
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях)
VERSION = "0.0.55"
VERSION = "0.0.56"
def setup_logging():
+6 -8
View File
@@ -50,17 +50,15 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
check("mapping.csv нет", "mapping.csv" not in names)
check("договор читается", "Договор" in zf.read("договор_obfuscated.md").decode("utf-8"))
# 3. ZIP с mapping.csv
# 3. ZIP с mapping.csv — mapping НЕ должен попадать в архив (секрет, хранится отдельно)
zip_data = build_zip(
[("file.md", b"data")],
mapping_csv="тип,оригинал,замена\nperson,Иванов,Иванов_0001\n"
)
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
names = zf.namelist()
check("mapping.csv есть", "mapping.csv" in names)
csv_content = zf.read("mapping.csv").decode("utf-8")
check("mapping с BOM", csv_content.startswith('\ufeff'))
check("данные в mapping", "Иванов_0001" in csv_content)
check("mapping.csv НЕ в архиве", "mapping.csv" not in names)
check("только file.md", len(names) == 1)
# 4. ZIP с кириллицей в именах
zip_data = build_zip([("договор_№123.md", "текст".encode("utf-8"))])
@@ -116,11 +114,11 @@ csv = build_mapping_csv(mapping)
zip_data = build_zip([("doc.md", "# Договор с Иванов_0001".encode("utf-8"))], mapping_csv=csv)
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
check("два файла в ZIP", len(zf.namelist()) == 2)
check("только doc.md в ZIP (без mapping)", len(zf.namelist()) == 1)
doc = zf.read("doc.md").decode("utf-8")
check("doc с заменой", "Иванов_0001" in doc)
csv_content = zf.read("mapping.csv").decode("utf-8")
check("CSV с обеими заменами", "Иванов_0001" in csv_content and "ООО_Технология_0001" in csv_content)
check("mapping.csv не в архиве", "mapping.csv" not in zf.namelist())
check("CSV отдаётся отдельной строкой", "Иванов_0001" in csv and "ООО_Технология_0001" in csv)
# ═══════════════════════════════════════════════════════════════