refactor(render): этап 1 — sanitizer.py + санитайз per-post (#28) #36
Reference in New Issue
Block a user
Delete Branch "refactor/render-stage1-sanitizer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Этап 1/6 эпика (#34, closes #28):
sanitizer.py(единственный bleach-конфиг) + санитайз per-post внутри_render_pipeline. Стекнут на этап 0 (#35) — база этого PR = веткаrefactor/render-stage0-golden, так что здесь виден только дельта этапа 1. Мержить ПОСЛЕ #35 (тогда перетаргечу на main).Один конфиг bleach на весь проект, один санитайз на пост, ноль thread-hop'ов (раньше конфиг скопирован трижды и разъехался; RSS-путь делал
to_thread+новыйCSSSanitizer+вложенную функцию НА КАЖДЫЙ пост, хотя_render_pipelineуже в worker-треде).§3 (реестр)
s/delв фидах; §3.2 fail-open→fail-closed (вкл. single-post/JSON); §3.3<hr class="post-divider">виден (join ПОСЛЕ санитайза); §3.4 несбалансированный фрагмент в границах своего поста; §3.5 гранулярный fail-closed; §3.16 три имени лога → одноhtml_sanitization_error+log_context.Оракул-дисциплина (главное)
Golden-эталоны обновлены, и КАЖДЫЙ изменённый байт привязан к §3. Net-diff тегов: только
+<s>/</s>(§3.1) и+<hr>(§3.3);<div>/</div>сбалансированы +/- (перемещение = §3.4); прочих тегов net-0. Независимая проверка: после снятия токенов §3.1/§3.3/§3.4 + нормализатора все 6 голденов побайтово old==new — ни одной незаявленной правки. Оракул этапа 0 зелёный.How verified
pip install -r requirements.txt(kurigram 2.2.23) — ок; полныйpytest tests/— 346 passed (+14 sanitizer-тестов),test_stage0_golden.py— 11 passed. Прогнал независимо + отдельное внутреннее ревью на безопасность санитайзера (fail-closed, отсутствие XSS-обхода — все выходные поверхности санитайзятся,strip=Trueвырезает script/onerror/javascript) и на golden-сплайс (сплайс flag-порядка/lastBuildDate ничего не скрыл — нормализуется оракулом; подтверждено).Awareness (не в scope этапа, не вектор):
post['title']не гоняется черезsanitize_html, но feedgen XML-экранирует title при сериализации, а в HTML-фид title не эмитится (join только поpost['html'], там body уже экранирован). Пре-существующее поведение.Checklist
🤖 Авто-ревью · #36 (refactor(render): этап 1 — sanitizer.py + санитайз per-post, #28), голова
3f4c7f0fВердикт: CHANGES. Рефактор отличный — security чист (fail-closed, безопасный superset, нет cross-post-bleed), регрессий нет (каждое изменение golden объяснено §3-реестром, ноль потери контента, §3.4 ЧИНИТ латентный баг вложенности div), thread-safe, гейт зелёный (346 passed). Два ограниченных Do (тест security-инварианта + устаревшие комменты). Эскалаций нет. (Стекнут на #35, диффил от базы stage-0.)
🔴 Do
[test-coverage] Нет именованного теста на §3.4 (изоляция фрагмента per-post) — security-инвариант, держится «by construction», но не залочен —
tests/test_sanitizer.py§3.4 гарантирует: несбалансированный/висячий тег в посте A нормализуется в ПРЕДЕЛАХ своего фрагмента и не заглатывает пост B (cross-post XSS-изоляция). Инвариант держится (каждый
post['html']— отдельныйbleach.clean, join ПОСЛЕ; подтвердил security-прогоном: bleach авто-балансит фрагмент), но все multi-output XSS-тесты используют историю из ОДНОГО сообщения — нет теста с ≥2 постами, где A несёт несбалансированный тег (<div><b>oops), а B ассертится целым. Golden может не покрывать (реальный корпус сбалансирован). Это этап 1/6 рефактор-эпика — будущий этап, склеивающий ПЕРЕД санитайзом, вернёт cross-post-bleed и пройдёт ВСЕ текущие тесты. Fix: тест — история из 2 постов, A с несбалансированным тегом → ассерт, что B цел И тег ограничен фрагментом A.[documentation/coherence] 4 коммента описывают удалённый «whole-feed sanitize для HTML» —
post_parser.py:641-642,802,1007;rss_generator.py:223-224Этап 1 убрал whole-feed HTML-проход (HTML теперь per-post, как RSS), но комменты всё ещё говорят «per-post for RSS, whole-feed for HTML». Вывод «санитайзится ровно один раз» остаётся верным (один раз, в
_render_pipeline), так что чисто формулировка — но нарушает правило §4 «обновляй устаревшие комменты». Fix: «whole-feed (HTML)» → «per-post для обоих (RSS и HTML) в_render_pipeline».✅ Верифицировано чистым (веер 9 аспектов) + ⛔ DROP
<); единый конфиг — безопасный superset (allow-list байт-в-байт как 3 старые копии, кроме инертныхs/delбез атрибутов;javascript:/onerror/data:svg— стрипаются); нет cross-post-bleed (единая точка_render_pipeline, bleach авто-балансит,<hr>— доверенный литерал).</div>на границах message-flags = §3.4, net-zero на фид (текст не терялся и не пересекал границы постов). §3.4 ФИКСИТ латентный баг: старый whole-feed копил несбалансированные open-div и сваливал закрывашки в один пост ниже (вкладывая посты друг в друга). Реестр полон, недокументированных изменений нет._render_pipelineтолько черезasyncio.to_thread; удалённый внутренний per-post hop был реальным двойным hop'ом — bleach остаётся вне loop'а); RSS стал БЫСТРЕЕ; module-level_CSS_SANITIZERтолько читается → thread-safe (bleach 6.4.0).pytest tests/— 346 passed (CI-fidelity: kurigram 2.2.23; 10 LIVE_PHOTO-ошибок при 2.2.22 — дрейф на НЕизменённом коде, не дефект PR).DROP / awareness (не блокеры):
[below-threshold]§3.3<hr>-divider именованным тестом не покрыт — но golden-регрессия его ловит (дивайдеры теперь в фикстурах). Ниже DROP-floor.[pre-existing]CSSwidth:expression()пропускается bleach'ем — идентично во ВСЕХ 3 старых конфигах, мёртвый IE<11-вектор, #36 не ухудшает.[operator-note]старые лог-имена (rss_html_sanitization_error/html_final_sanitization_error) свёрнуты вhtml_sanitization_error(§3.16) — алертинг по старым именам обновить.Веер 9 аспектов (4 субагента + docs/conv/simpl/arch напрямую); golden-аудит полный (каждое изменение §3-объяснено, ноль потери контента), security fail-closed проверен эмпирически, §3.1/§3.3 сверил в диффе сам. Рефактор здоровый — Do это security-инвариант-тест на эпик + чистка комментов.
Round 1 — оба Do закрыты.
1. [test-coverage] §3.4 изоляция. Отличный улов — инвариант держался by construction, но не был залочен. Добавил
test_unbalanced_post_does_not_swallow_next_post_html_feed(вtest_stage4_eventloop.py, гоняет реальный per-post HTML-пайплайн черезmake_message+generate_channel_html): история из 2 постов, A с висячим<div><b>DANGLING_A(без закрывашек, сырьём в pre-sanitize body как в существующих XSS-тестах), B целый с маркером. Ассертит: фид бьётся на 2 фрагмента по<hr post-divider>;<div>/<b>A сбалансированы в пределах фрагмента A; маркер B — в фрагменте B и НЕ в A.Ключевое — проверил, что тест краснеет на регрессии join-before-sanitize: симулировал этот путь (склейка raw-постов → один whole-feed
bleach.clean) → невайтлистнутый<hr>стрипаетсяstrip=True→ 0 разделителей →len(parts)==1→ первый ассерт падает. То есть будущий этап, вернувший склейку ДО санитайза, этим тестом ловится (не через прямой вызовsanitize_html, а через реальный пайплайн).2. [documentation] Устаревшие комменты. 4 коммента про удалённый «whole-feed HTML sanitize» → «per-post в
_render_pipelineдля обоих (RSS и HTML)» (post_parser.py:~641/802/1007, rss_generator.py:~223). Только текст комментов/докстринга, исполняемый код не тронут.Проверка: полный
pytest tests/— 347 passed (+1);test_stage0_golden.py— 11 passed, golden не менялся. Ставлю review/needs.🤖 Авто-ревью · раунд 2 · #36 (refactor(render): этап 1 — sanitizer.py + санитайз per-post, #28), голова
711cb2cdВердикт: PASS ✅ (review/approved). Оба Do round-1 закрыты ровно рекомендованным способом, нового бага нет.
Проверено адверсариально
test_unbalanced_post_does_not_swallow_next_post_html_feed— пост A несёт висячий<div><b>без закрывашек, пост B корректен. Ассертит: (а) ровно 2 top-level фрагмента (split по<hr class="post-divider">→len(parts)==2; join-before-sanitize регрессия стрипнула бы дивайдер); (б) теги A сбалансированы внутри frag_a (frag_a.count("<div")==count("</div>"), то же для<b>; rejoin-before-sanitize отложил бы закрывашки за дивайдер, оставив frag_a с перекосом). Мутационно-релевантен — падал бы ровно при той будущей регрессии эпика, что я флагнул. Не тавтология._render_pipelineдля ОБОИХ (RSS и HTML)» (post_parser.py+rss_generator.py).pytest tests/347 passed (346 + 1 новый §3.4) на тех же фикстурах ⇒ прод-поведение неизменно (комменты behavior-neutral). Нового бага нет.Остальное (security fail-closed + безопасный superset + нет cross-post-bleed; golden-аудит: каждое изменение §3-объяснено, ноль потери контента, §3.4 чинит латентный баг; stability loop-safety+thread-safe; coherence) — неизменно с round-1, всё LGTM. DROP-заметки round-1 (hr-тест покрыт golden'ом, CSS
expression()до-существующий) в силе.Фикс-раунд: оба Do закрыты, §3.4-инвариант заперт мутационно-релевантным тестом (падал бы при join-before-sanitize), комменты актуализированы, гейт 347 passed, goldens не тронуты. Этап 1 эпика #34 чист.