Files
ObsiGate/docs/features/secret-mask-188.md
T
bruno e01e837a2a
CI / lint (push) Successful in 2m40s
CI / security (push) Successful in 1m33s
CI / test (push) Successful in 4m19s
CI / build (push) Successful in 1m31s
CI / e2e (push) Successful in 16m56s
feat: secrets masqués — couverture universelle clés API/mots de passe + clic pour copier (#188)
2026-10-08 13:22:27 -04:00

3.0 KiB

Fiche — #188 · Secrets masqués : couverture universelle + clic pour copier

  • Statut : ✅ livré le 2026-10-08 (ouvert le 2026-10-08)
  • Effort : ~1 jour · Impact : 🟡
  • Zones : backend/secret_redactor.py, backend/render.py, backend/routers/files_read.py, frontend/js/viewer.js, frontend/style.css, frontend/locales/{fr,en}.json

Problème

Le redacteur (secret_redactor.py) ne masquait que les JWT, les clés sk-/pk-/rk-, AKIA…, ghp_…, les connection strings et les affectations api_key=/token=/secret= de 20 caractères ou plus. Résultat : la majorité des clés API réelles (Google, Slack, Stripe, GitLab, Hugging Face, npm…) et tous les mots de passe courts passaient en clair dans l'aperçu markdown, et un masque affiché ([CLÉ API MASQUÉE]) n'était pas récupérable : il fallait ouvrir la source pour relire la valeur.

Solution

  1. Détection universelle — une alternation unique de formats fournisseur (OpenAI/Anthropic sk-, Stripe sk_live_/whsec_, GitLab glpat-, GitHub ghp_/github_pat_, Google AIza…/ya29., AWS AKIA/ASIA, Slack xoxb-, SendGrid SG., Hugging Face hf_, npm, Docker, Resend, Square, Atlassian, Discord, Telegram, Bearer …) en plus des motifs déjà présents ; le plancher des affectations génériques passe de 20 à 8 caractères.
  2. Mots de passe — motif dédié ([MOT DE PASSE MASQUÉ]) valable toute longueur et toute quote (password=, "passwd": "…", db_password=, passphrase=, mot de passe=), qualificateur inclus. PWD= (dossier de travail shell) reste épargné : pwd n'est reconnu que qualifié.
  3. Clic → copie — le pipeline de rendu passe par des placeholders (\uE000n\uE001) qui survivent à la conversion markdown (un <span> littéral dans un bloc de code serait affiché tel quel), puis restore_masks() les réécrit après le rendu :
    • aperçu authentifié (GET /api/file/…, click_to_copy=True) : <span class="secret-mask" data-secret="valeur">[CLÉ API MASQUÉE]</span> — la valeur n'est jamais affichée, un clic la copie (listener délégué unique sur la zone de contenu, toast i18n FR/EN, infobulle) ;
    • partages publics, exports PDF, contexte IA / MCP : libellé seul, la valeur ne quitte pas le serveur. Les blocs <pre><code> portant un masque ne sont pas re-colorés (highlight.js reconstruirait le markup et perdrait le badge).

Hors périmètre (volontaire)

  • La vue « source » (fichier brut) et les aperçus de fichiers non-markdown ne sont pas masqués : c'est le fichier lui-même qui est affiché, le redacteur ne s'y appliquait jamais.
  • Le serveur MCP / l'assistant IA continuent de recevoir les libellés seuls (inchangé).

Tests

tests/test_api_main.py::TestSecretRedactor : formats fournisseurs (paramétrés), mots de passe (paramétrés), PWD= non masqué, round-trip placeholder → span cliquable, rendu markdown cliquable vs libellé seul.