From f12c03588deff030a5dde272708d9ea73b17add4 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sat, 13 Jun 2026 09:56:58 +0500 Subject: [PATCH] =?UTF-8?q?fix(llm):=20close=20=C2=A719=20redaction=20rege?= =?UTF-8?q?x=20gaps=20for=20bare=20phone=20/=20CAPS=20name=20/=20bare=20SN?= =?UTF-8?q?ILS=20(#1207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_PHONE_RE` требовал префикс +7/8 + хотя бы один разделитель → copy-paste форма «79221234567»/«89221234567» (самая частая) уходила в OpenAI как есть. `_FULLNAME_RE` требовал строчные после первой буквы → стандарт ЕГРН/паспортных выписок «ИВАНОВ ИВАН ИВАНОВИЧ» не матчил. СНИЛС без разделителей не покрывался вообще (`_SNILS_RE` ждёт формат «NNN-NNN-NNN NN»). Для chat-пути scrub — единственный барьер перед OpenAI (SafePayload.text без allowlist'а), при llm_enabled=True PII покидает РФ (нарушение §19/152-ФЗ). Patch: - _PHONE_BARE_RE: (? None: assert "Иванович" not in out +# ── #1207: regex-дыры в первичном scrub'е (P2-security) ───────────────────────── + + +def test_scrub_phone_bare_11_digits_starting_with_7() -> None: + """Copy-paste форма «79221234567» (11 цифр, без + и без 8) — самая частая дыра. + + _PHONE_RE требует «+7»/«\\b8» + разделитель → этот вариант пропускался и уходил + в OpenAI как есть. _PHONE_BARE_RE закрывает дыру. + """ + out = scrub_text("позвоните 79221234567 договоримся") + assert "[REDACTED:phone]" in out + assert "79221234567" not in out + + +def test_scrub_phone_bare_11_digits_starting_with_8() -> None: + """Аналогично — «89221234567» без разделителей.""" + out = scrub_text("моб 89221234567 в любое время") + assert "[REDACTED:phone]" in out + assert "89221234567" not in out + + +def test_scrub_fullname_caps_format() -> None: + """Стандарт ЕГРН/паспортных выписок «ИВАНОВ ИВАН ИВАНОВИЧ» — старый regex требовал + строчные после первой буквы → каполная форма уходила как есть.""" + out = scrub_text("собственник ИВАНОВ ИВАН ИВАНОВИЧ зарегистрирован") + assert "[REDACTED:name]" in out + assert "ИВАНОВИЧ" not in out + + +def test_scrub_fullname_mixed_caps_and_titlecase() -> None: + """Смешанная форма «ИВАНОВ Иван Иванович» — тоже должна срабатывать.""" + out = scrub_text("по доверенности ИВАНОВ Иван Иванович подписал") + assert "[REDACTED:name]" in out + assert "ИВАНОВ" not in out + + +def test_scrub_snils_bare_11_digits() -> None: + """СНИЛС без разделителей «12345678901» — не покрывался ни phone (нет 7/8 префикса), + ни ИНН (длина не 10/12).""" + out = scrub_text("СНИЛС 12345678901 в личном деле") + assert "[REDACTED:snils]" in out + assert "12345678901" not in out + + +def test_phone_bare_wins_over_snils_bare_for_7_prefix() -> None: + """Конфликт _PHONE_BARE_RE vs _SNILS_BARE_RE: 11 цифр, начинаются с 7 → phone.""" + out = scrub_text("79221234567") + assert "[REDACTED:phone]" in out + assert "[REDACTED:snils]" not in out + + +def test_inn_10_digits_not_caught_as_snils_bare() -> None: + """10-значный ИНН не должен попадать в _SNILS_BARE_RE (тот требует ровно 11).""" + out = scrub_text("ИНН 7707083893 действующий") + assert "[REDACTED:inn]" in out + assert "[REDACTED:snils]" not in out + + def test_scrub_multiple_pii_in_one_text() -> None: out = scrub_text("Иванов Иван Иванович, +7 912 345 67 89, mail@x.ru") assert "[REDACTED:name]" in out