File size: 7,552 Bytes
bad7a01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""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"
            )