feat: add sanitize_plaintext for XMPP text sanitization (M10-U3)
Add sanitize_plaintext(text: str) -> str to utils/text.py for preparing XMPP plain-text message bodies from untrusted Pronote/AI content. - Strips HTML tags via BeautifulSoup (html.parser) - Strips C0, DEL, and C1 control characters (preserves \t, \n, \r) - Preserves Unicode including emojis (📌📅📚💬📢) - Idempotent: f(f(x)) == f(x) - Addresses SEC-XMPP-06: XMPP injection hardening 37 unit tests covering HTML, entities, control chars, emojis, idempotence. Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid> Co-authored-by: opencode/coder <coder@agents.invalid>
This commit is contained in:
@@ -10,7 +10,9 @@ from __future__ import annotations
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
__all__ = ["normalize_subject"]
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
__all__ = ["normalize_subject", "sanitize_plaintext"]
|
||||
|
||||
|
||||
def normalize_subject(subject: str) -> str:
|
||||
@@ -30,3 +32,29 @@ def normalize_subject(subject: str) -> str:
|
||||
normalized = re.sub(r"[^\w\s]", "", normalized)
|
||||
normalized = re.sub(r"\s+", " ", normalized).strip()
|
||||
return normalized.lower()
|
||||
|
||||
|
||||
# Pattern des caractères de contrôle ASCII non imprimables (à l'exception
|
||||
# des tabulations ``\\t``, des sauts de ligne ``\\n`` et des retours chariot ``\\r``).
|
||||
_CONTROL_CHARS_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
|
||||
|
||||
|
||||
def sanitize_plaintext(text: str) -> str:
|
||||
"""Prépare un texte pour le corps de message XMPP en texte brut.
|
||||
|
||||
Supprime les balises HTML (via ``BeautifulSoup`` avec le parseur
|
||||
``html.parser``) puis les caractères de contrôle ASCII non imprimables,
|
||||
à l'exception des tabulations (``\\t``), des sauts de ligne (``\\n``) et
|
||||
des retours chariot (``\\r``). Les caractères Unicode au-delà de ``\\x1f``,
|
||||
notamment les emojis, sont conservés. La transformation est idempotente :
|
||||
appliquée deux fois, elle produit le même résultat qu'appliquée une seule
|
||||
fois. Une chaîne vide donne une chaîne vide.
|
||||
|
||||
:param text: Le texte brut ou HTML à assainir.
|
||||
:return: Le texte assaini, sans balises HTML ni caractères de contrôle.
|
||||
:rtype: str
|
||||
"""
|
||||
# Étape 1 : suppression des balises HTML.
|
||||
plain = BeautifulSoup(text, "html.parser").get_text()
|
||||
# Étape 2 : suppression des caractères de contrôle.
|
||||
return _CONTROL_CHARS_RE.sub("", plain)
|
||||
|
||||
Reference in New Issue
Block a user