Picarones / tests /security /test_s1_xss_in_reports.py
Claude
fix(sprint-S1.1)!: corriger XSS critique via Jinja2 autoescape=False (Bandit B701, CWE-94)
bad7a01 unverified
Raw
History Blame
7.55 kB
"""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 "&lt;script&gt;alert(" in html or "&#x3c;script&#x3e;" in html.lower(), (
"Le ``<`` dans corpus_name doit รชtre รฉchappรฉ en ``&lt;``."
)
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 ``&quot;`` ou
# ``&#34;`` 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"
)