From 670f533f4885fa35c1f328369cc260ebf2ea3b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 20 Aug 2026 09:38:28 +0300 Subject: [PATCH] =?UTF-8?q?v0.0.56:=20=D0=9A=D0=A0=D0=98=D0=A2=D0=98=D0=A7?= =?UTF-8?q?=D0=9D=D0=9E=20=E2=80=94=20mapping.csv=20(=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=20=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=84=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8)=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D0=B2=D1=8B=D1=85=D0=BE=D0=B4=D0=BD=D0=BE=D0=B3=D0=BE=20ZIP?= =?UTF-8?q?,=20CSV=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- History/2026-08-20-csv-out-of-zip.md | 24 ++++++++++++++++++++++++ drhider/builder.py | 16 +++++----------- site/app.py | 2 +- tests/test_builder.py | 14 ++++++-------- 4 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 History/2026-08-20-csv-out-of-zip.md diff --git a/History/2026-08-20-csv-out-of-zip.md b/History/2026-08-20-csv-out-of-zip.md new file mode 100644 index 0000000..467d53c --- /dev/null +++ b/History/2026-08-20-csv-out-of-zip.md @@ -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/` (кнопка «Скачать 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. diff --git a/drhider/builder.py b/drhider/builder.py index c2b5ac2..7566b6c 100644 --- a/drhider/builder.py +++ b/drhider/builder.py @@ -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() diff --git a/site/app.py b/site/app.py index 134da88..f8d9118 100644 --- a/site/app.py +++ b/site/app.py @@ -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(): diff --git a/tests/test_builder.py b/tests/test_builder.py index fc06478..0bbe6b4 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -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) # ═══════════════════════════════════════════════════════════════