Cartes 600, 610 et 620 closes (back-office XR, ressource 360, appairage tablette), 250 et 320 retirées du planifié, 015 et 330 à jour ; STATUS, roadmap, test-plan, todo-features et plans VR / frontière immersif alignés ; kanban.html regénéré. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VxSQeGQYUvPmSEoGdnidA
4.6 KiB
title, area, tags, flag, src
| title | area | tags | flag | src |
|---|---|---|---|---|
| L'API publique répond <strong>sans clé API</strong> | backend | manager-service, sécurité | critical | Le contenu de n'importe quelle instance est lisible par qui connaît son identifiant | conversation 08/09 — vérification de la préprod depuis internet · audit complet 12/09 |
Constaté le 08/09 sur la préprod, depuis internet, sans authentification :
curl "https://api.mymuseum.be/api/Configuration?instanceId=633ee379d9405f32f166f047" → 200, avec le contenu complet et ses traductions
La même requête avec X-Api-Key renvoie le même 200. Cause exacte, vérifiée le 08/09 : Get (ConfigurationController.cs:52) et GetDetailAsync (:117) portent un [AllowAnonymous] explicite. Ce n'est donc pas une clé mal vérifiée, c'est une ouverture assumée — probablement héritée de la v2, où les apps visiteur n'avaient pas de clé. Les apps l'envoient pourtant (client.dart:54), et le mécanisme existe et fonctionne : ApiKeyAuthenticationHandler + la policy AppReadAccess, déjà utilisée par byPin (:83) et export (:383) du même contrôleur.
Ce que ça expose : le contenu éditorial de n'importe quelle instance, à qui connaît un instanceId — lequel se lit en clair dans un APK de flavor, ou se devine à partir d'un export. Pas de données personnelles, mais tout le travail éditorial d'un client.
✅ Audit complet fait le 12/09 — 40 routes anonymes recensées, dont 2 seulement contrôlent la clé (Configuration/{id}/export et Instance/{id}, tous deux corrigés en septembre). Le trou est donc général, pas ponctuel : 24 routes de contenu sont ouvertes — Configuration (2), Section (4), Resource (2), SectionMap (3), SectionEvent (3), SectionAgenda (2), SectionParcours (2), SectionQuiz (1), ApplicationInstance (2).
🔴 Trouvaille plus grave que la carte, corrigée le 12/09. GET /api/Instance/slug/{slug} était anonyme et sans filtrage : il rendait le pinCode — celui qui ouvre l'appairage des tablettes et des casques — plus l'adresse de facturation, le numéro de TVA, les quotas et le plan du client. Et le slug n'est pas un secret : c'est l'URL du site visiteur (app.myinfomate.be/{slug}). La fonction de filtrage StripCommercialFields existait déjà, avec un commentaire qui nommait le risque du pinCode — elle n'était simplement pas appelée ici. Corrigé sur slug et sur byPin, avec 3 tests qui verrouillent le contrat (243 tests au vert). Deux champs restent exposés à dessein : publicApiKey, sans laquelle visitapp-web ne peut pas s'amorcer, et isTrialActive, qui sert au filigrane d'essai affiché au visiteur.
⚠️ Ce que fermer les 24 routes apportera vraiment — et ce que ça n'apportera pas. La clé s'obtient anonymement : par le slug (qui est dans l'URL) ou par app-key avec le pincode. Après fermeture, le contenu ne sera donc pas confidentiel : il sera lisible par qui lit une URL, au lieu de qui devine un instanceId. Le gain réel est ailleurs, et il compte : plus d'énumération par identifiant, un accès tracé et révocable, et un seul chemin d'entrée au lieu de vingt-quatre. Rendre le contenu réellement privé serait un autre chantier, avec une autre décision produit.
Reste à faire : fermer les 24 routes de contenu. Deux obstacles connus. Un : [Authorize] de classe et d'action se combinent — c'est pour ça qu'export passe par [AllowAnonymous] + contrôle manuel ; il faut donc un attribut réutilisable, pas un simple changement de policy. Deux : deux routes doivent rester anonymes et c'est écrit dans le code de visitapp-web — instance/slug/{slug} (l'amorce, qui distribue la clé) et ApplicationInstance?instanceId= (la page /download, où le visiteur arrive d'un QR imprimé sans slug ni clé).