Spaces:
Sleeping
Sleeping
Claude
fix(sprint-S1.1)!: corriger XSS critique via Jinja2 autoescape=False (Bandit B701, CWE-94)
bad7a01 unverified | """Sprint S1.3 โ XSS dans le rapport HTML gรฉnรฉrรฉ. | |
| Vรฉrifie que tout contenu utilisateur (corpus_name, sentences narratives, | |
| nom de moteur) est รฉchappรฉ HTML dans le rapport produit par | |
| ``picarones.reports.html.ReportGenerator``. | |
| Bandit B701 (CWE-94) avait flaggรฉ ``Environment(autoescape=False)`` โ | |
| ce test concrรฉtise l'attaque que l'audit dรฉcrivait : un corpus nommรฉ | |
| ``</title><script>alert(1)</script>`` doit รชtre inerte dans le HTML. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from picarones.evaluation.benchmark_result import ( | |
| BenchmarkResult, | |
| DocumentResult, | |
| EngineReport, | |
| ) | |
| def _make_minimal_benchmark( | |
| corpus_name: str, | |
| engine_name: str = "tesseract", | |
| ) -> BenchmarkResult: | |
| """Construit un BenchmarkResult minimal valide avec le ``corpus_name`` fourni.""" | |
| from picarones.evaluation.metric_result import MetricsResult | |
| metrics = MetricsResult(cer=0.0, wer=0.0, mer=0.0, wil=0.0) | |
| doc = DocumentResult( | |
| doc_id="doc01", | |
| image_path="doc01.png", | |
| ground_truth="Bonjour", | |
| hypothesis="Bonjour", | |
| metrics=metrics, | |
| duration_seconds=0.1, | |
| ) | |
| engine = EngineReport( | |
| engine_name=engine_name, | |
| engine_version="5.3.0", | |
| engine_config={}, | |
| document_results=[doc], | |
| ) | |
| return BenchmarkResult( | |
| corpus_name=corpus_name, | |
| corpus_source=None, | |
| document_count=1, | |
| engine_reports=[engine], | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 1. corpus_name avec script tag โ doit รชtre รฉchappรฉ dans <title> | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestCorpusNameXSS: | |
| """Le ``corpus_name`` est injectรฉ dans ``<title>`` de | |
| ``base.html.j2``. Sans รฉchappement, un nom malicieux compromet | |
| tout rapport partagรฉ.""" | |
| def test_script_tag_in_corpus_name_is_escaped(self, tmp_path: Path) -> None: | |
| from picarones.reports.html import ReportGenerator | |
| bench = _make_minimal_benchmark( | |
| corpus_name="</title><script>alert('xss')</script>", | |
| ) | |
| gen = ReportGenerator(bench) | |
| out = tmp_path / "report.html" | |
| gen.generate(out) | |
| html = out.read_text() | |
| # Le tag </title> ne doit PAS apparaรฎtre au milieu du HTML | |
| # (autre que celui lรฉgitime ร la fin du <head>) au point | |
| # d'attacher un <script>. | |
| assert "<script>alert('xss')</script>" not in html, ( | |
| "XSS confirmรฉ : le script malicieux est exรฉcutable dans le rapport.\n" | |
| "Causes possibles : autoescape=False dans Jinja2 + corpus_name " | |
| "non filtrรฉ." | |
| ) | |
| # Forme correcte : caractรจres dangereux รฉchappรฉs. | |
| assert "<script>alert(" in html or "<script>" in html.lower(), ( | |
| "Le ``<`` dans corpus_name doit รชtre รฉchappรฉ en ``<``." | |
| ) | |
| def test_html_attribute_injection_in_corpus_name(self, tmp_path: Path) -> None: | |
| """Cas d'attaque attribute-based : ``" onerror=alert(1)`` | |
| peut casser une balise si corpus_name est utilisรฉe dans | |
| un attribut.""" | |
| from picarones.reports.html import ReportGenerator | |
| bench = _make_minimal_benchmark( | |
| corpus_name='" onerror="alert(1)" foo="', | |
| ) | |
| gen = ReportGenerator(bench) | |
| out = tmp_path / "report.html" | |
| gen.generate(out) | |
| html = out.read_text() | |
| # Le caractรจre ``"`` doit รชtre รฉchappรฉ en ``"`` ou | |
| # ``"`` partout oรน corpus_name est rendu. Aucune | |
| # attribute injection ne doit subsister. | |
| assert ' onerror="alert(' not in html, ( | |
| "Attribute injection : un guillemet non รฉchappรฉ permet " | |
| "d'injecter onerror=." | |
| ) | |
| def test_corpus_name_with_unicode_renders_correctly( | |
| self, tmp_path: Path, | |
| ) -> None: | |
| """Corollaire โ vรฉrifie qu'un nom Unicode lรฉgitime | |
| (``Manuscrit mรฉdiรฉval โ chartes XIIIยฐ``) reste lisible | |
| aprรจs รฉchappement.""" | |
| from picarones.reports.html import ReportGenerator | |
| bench = _make_minimal_benchmark( | |
| corpus_name="Manuscrit mรฉdiรฉval โ chartes XIIIยฐ", | |
| ) | |
| gen = ReportGenerator(bench) | |
| out = tmp_path / "report.html" | |
| gen.generate(out) | |
| html = out.read_text() | |
| # Caractรจres Unicode safe doivent rester intacts. | |
| assert "Manuscrit mรฉdiรฉval" in html | |
| assert "XIIIยฐ" in html | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 2. Engine name (moteur OCR) avec injection | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestEngineNameXSS: | |
| """Le nom de moteur peut venir d'un import HuggingFace ou d'une | |
| config utilisateur. Doit รชtre รฉchappรฉ dans tous les renderers | |
| qui l'affichent.""" | |
| def test_engine_name_with_script_is_escaped(self, tmp_path: Path) -> None: | |
| from picarones.reports.html import ReportGenerator | |
| bench = _make_minimal_benchmark( | |
| corpus_name="test", | |
| engine_name="<script>alert('engine')</script>", | |
| ) | |
| gen = ReportGenerator(bench) | |
| out = tmp_path / "report.html" | |
| gen.generate(out) | |
| html = out.read_text() | |
| assert "<script>alert('engine')</script>" not in html, ( | |
| "Engine name XSS : un nom de moteur malicieux est exรฉcutรฉ." | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 3. Bandit B701 ne doit plus signaler autoescape=False | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestJinja2EnvIsAutoescaped: | |
| """Garde-fou contre la rรฉgression : ``_build_jinja_env`` doit | |
| retourner un Environment avec autoescape activรฉ pour les | |
| extensions HTML/J2.""" | |
| def test_env_has_autoescape_enabled_for_html(self) -> None: | |
| from picarones.reports.html.generator import _build_jinja_env | |
| env = _build_jinja_env() | |
| # autoescape doit รชtre un Callable (select_autoescape) ou True, | |
| # pas False ni None. | |
| autoescape = env.autoescape | |
| assert autoescape, ( | |
| f"Jinja2 Environment.autoescape={autoescape!r} โ XSS exposรฉ." | |
| ) | |
| # Si c'est une fonction (select_autoescape), elle doit | |
| # retourner True pour les HTML/J2. | |
| if callable(autoescape): | |
| assert autoescape("base.html.j2"), ( | |
| "select_autoescape doit activer l'รฉchappement pour .j2" | |
| ) | |
| assert autoescape("any.html"), ( | |
| "select_autoescape doit activer l'รฉchappement pour .html" | |
| ) | |