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
21 lines
4.6 KiB
Markdown
21 lines
4.6 KiB
Markdown
---
|
|
title: L'API publique répond <strong>sans clé API</strong>
|
|
area: backend
|
|
tags: manager-service, sécurité
|
|
flag: critical | Le contenu de n'importe quelle instance est lisible par qui connaît son identifiant
|
|
src: conversation 08/09 — vérification de la préprod depuis internet · audit complet 12/09
|
|
---
|
|
<p>Constaté le 08/09 sur la préprod, depuis internet, sans authentification :</p>
|
|
<pre>curl "https://api.mymuseum.be/api/Configuration?instanceId=633ee379d9405f32f166f047"
|
|
→ 200, avec le contenu complet et ses traductions</pre>
|
|
<p>La même requête <strong>avec</strong> <code>X-Api-Key</code> renvoie le même 200. <strong>Cause exacte, vérifiée le 08/09</strong> : <code>Get</code> (<code>ConfigurationController.cs:52</code>) et <code>GetDetailAsync</code> (<code>:117</code>) portent un <code>[AllowAnonymous]</code> <em>explicite</em>. 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 (<code>client.dart:54</code>), et le mécanisme existe et fonctionne : <code>ApiKeyAuthenticationHandler</code> + la policy <code>AppReadAccess</code>, déjà utilisée par <code>byPin</code> (<code>:83</code>) et <code>export</code> (<code>:383</code>) du même contrôleur.</p>
|
|
<p><strong>Ce que ça expose</strong> : le contenu éditorial de n'importe quelle instance, à qui connaît un <code>instanceId</code> — 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.</p>
|
|
|
|
<p>✅ <strong>Audit complet fait le 12/09</strong> — <strong>40 routes anonymes</strong> recensées, dont <strong>2 seulement contrôlent la clé</strong> (<code>Configuration/{id}/export</code> et <code>Instance/{id}</code>, tous deux corrigés en septembre). Le trou est donc <strong>général, pas ponctuel</strong> : 24 routes de contenu sont ouvertes — <code>Configuration</code> (2), <code>Section</code> (4), <code>Resource</code> (2), <code>SectionMap</code> (3), <code>SectionEvent</code> (3), <code>SectionAgenda</code> (2), <code>SectionParcours</code> (2), <code>SectionQuiz</code> (1), <code>ApplicationInstance</code> (2).</p>
|
|
|
|
<p>🔴 <strong>Trouvaille plus grave que la carte, corrigée le 12/09.</strong> <code>GET /api/Instance/slug/{slug}</code> était anonyme <strong>et sans filtrage</strong> : il rendait le <strong>pinCode</strong> — celui qui ouvre l'appairage des tablettes et des casques — plus l'<strong>adresse de facturation</strong>, le <strong>numéro de TVA</strong>, les quotas et le plan du client. Et le slug n'est pas un secret : <strong>c'est l'URL du site visiteur</strong> (<code>app.myinfomate.be/{slug}</code>). La fonction de filtrage <code>StripCommercialFields</code> existait déjà, avec un commentaire qui nommait le risque du pinCode — elle n'était simplement pas appelée ici. Corrigé sur <code>slug</code> et sur <code>byPin</code>, avec 3 tests qui verrouillent le contrat (243 tests au vert). Deux champs restent exposés à dessein : <code>publicApiKey</code>, sans laquelle <code>visitapp-web</code> ne peut pas s'amorcer, et <code>isTrialActive</code>, qui sert au filigrane d'essai affiché au visiteur.</p>
|
|
|
|
<p>⚠️ <strong>Ce que fermer les 24 routes apportera vraiment — et ce que ça n'apportera pas.</strong> La clé s'obtient <strong>anonymement</strong> : par le slug (qui est dans l'URL) ou par <code>app-key</code> 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 <code>instanceId</code>. Le gain réel est ailleurs, et il compte : plus d'énumération par identifiant, un accès tracé et <strong>révocable</strong>, 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.</p>
|
|
|
|
<p><strong>Reste à faire</strong> : fermer les 24 routes de contenu. Deux obstacles connus. <strong>Un</strong> : <code>[Authorize]</code> de classe et d'action se <strong>combinent</strong> — c'est pour ça qu'<code>export</code> passe par <code>[AllowAnonymous]</code> + contrôle manuel ; il faut donc un attribut réutilisable, pas un simple changement de policy. <strong>Deux</strong> : deux routes doivent <strong>rester</strong> anonymes et c'est écrit dans le code de <code>visitapp-web</code> — <code>instance/slug/{slug}</code> (l'amorce, qui distribue la clé) et <code>ApplicationInstance?instanceId=</code> (la page <code>/download</code>, où le visiteur arrive d'un QR imprimé sans slug ni clé).</p>
|