Date : 15/02/2026 Status : IMPLEMENTEE Auditeur : Claude Opus 4.6 (synthese confrontee) Sources : Pass 1 (02h00), Pass 2 (03h00), Pass 3 (04h00)
| Categorie | Nombre |
|---|---|
| Findings CONFIRMES (2-3 passes concordantes) | 12 |
| Findings UNIQUES (1 seule passe) | 13 |
| Faux positifs elimines | 4 |
| Contradictions resolues | 3 |
Total findings retenus : 22 (12 confirmes + 10 uniques valides) Total faux positifs : 4 (dont 2 declares par les passes elles-memes)
setInterval dans ConversationSidebar.tsx (lignes 151-156) capture origShowArchived a l'initialisation. Apres le premier toggle du signal showArchived, la condition showArchived.value !== origShowArchived reste toujours vraie car origShowArchived n'est jamais mis a jour. Resultat : loadConversations() est appele toutes les 500ms indefiniment. De plus, le bouton "Voir archivees" (lignes 537-547) fait deja son propre fetch dans le handler onClick$, donc ce polling est totalement redondant meme quand il detecte correctement le premier changement./api/conversations toutes les 500ms, charge reseau et serveur continue.javascript: URI dans les liens MarkdownrenderMarkdown() dans markdown.ts applique escapeHtml() en premier (correct), puis la regex des liens [text](url) reconstruit un <a href="$2">. Or javascript:alert(1) ne contient aucun caractere HTML special (<, >, &, "), donc escapeHtml() le laisse intact. Le lien genere <a href="javascript:alert(1)"> est injecte via dangerouslySetInnerHTML. Le contenu provient des reponses du LLM — un agent compromis ou une injection dans les tool_results pourrait generer ce type de lien.<a href="javascript:..."> cliques par l'utilisateur — seuls les javascript: dans les barres d'adresse sont bloques. Pass 2 et Pass 3 avaient raison de classer BUG CERTAIN.window.location.href = '/' perd le message envoye dans retryObjectiveretryObjective dispatche ulias:send puis fait immediatement window.location.href = '/'. Le full page reload detruit tous les event listeners et le WebSocket AVANT que le message ait le temps d'etre envoye. Le message est perdu systematiquement. De plus, le reload force une reconnexion WS complete (re-auth, re-load conversation), degradant l'experience utilisateur.setTimeout(500) pour attendre que ChatMessages soit monte. Si le composant met plus de 500ms a se monter (appareil lent, tab en arriere-plan, reseau lent), l'evenement ulias:load-history est dispatche mais personne ne l'ecoute — l'historique ne s'affiche pas. A l'inverse, sur une machine rapide, l'utilisateur voit un chat vide pendant 500ms inutilement./api/teams ne passe pas le header X-API-Key, contrairement a toutes les autres pages. Pass 3 a verifie le backend : l'endpoint /api/teams ne verifie PAS l'authentification cote serveur non plus. Le code fonctionne donc, mais la liste des equipes et agents est accessible sans authentification.catch { /* silent */ }. Aucune erreur reseau n'est affichee a l'utilisateur. Les fichiers concernes : layout.tsx (5 occurrences), ConversationSidebar.tsx (11 occurrences), decisions/index.tsx, history/index.tsx, EventCard.tsx. Le cas le plus grave est le deleteConversation (C-07) ou l'etat local est modifie meme si le serveur a refuse l'operation.fetch DELETE, l'etat local est modifie (conversations.value = conversations.value.filter(...)) SANS verifier res.ok. Si le serveur repond 403, 404 ou 500, le fetch ne throw pas (seules les erreurs reseau throw). La conversation disparait de la vue mais existe toujours cote serveur. Meme probleme pour archiveConversation, togglePin, saveRename, et deleteObjective dans history/index.tsx.deleteConversation (ConversationSidebar.tsx:211-225) et deleteObjective (history/index.tsx:85-95) executent le DELETE immediatement sans confirm() ni modal de confirmation. Un clic accidentel sur le bouton supprimer detruit la donnee definitivement.ConnectionBadge est en lecture seule. L'utilisateur doit recharger la page manuellement. Le compteur reconnectAttempts est correctement reset a 0 sur auth_success, mais une panne serveur de plus de 31 secondes bloque l'utilisateur.send et sendCommand), pas seulement dans le message d'auth initial. Si les logs serveur capturent les messages WS, la cle est exposee en clair dans chaque entree.Confirme par : Pass 2 (2.5) + Pass 3 (F-01, F-02, F-03, F-04)
Severite definitive : BUG CERTAIN
Description consolidee : Plusieurs interfaces dans types.ts ne declarent pas tous les champs emis par le backend :
confidence, reasoning, multi_step, pipeline, modelmodel, agent, toolsduration, successmodelduration, toolCalls, iterationsset_conversation, decision_responsedecision_requestLe code accede a ces champs via event.data?.field ou as any, contournant TypeScript. Cela fonctionne a l'execution car les objets JSON bruts contiennent ces champs, mais la type safety est nulle.
Impact : Aucun bug fonctionnel actuel, mais le code est fragile — un renommage cote backend passera silencieusement. Le compilateur TypeScript ne protege pas le developppeur.
Priorite de correction : MOYENNE
useStore<StreamingInfo> dans ChatMessages.tsx (ligne 22) est mute directement dans un handler d'event DOM vanilla (pas dans un handler Qwik $()). Les trois passes s'accordent : le pattern fonctionne en pratique car les mutations se font dans un useVisibleTask$ (code client-only), et le store est initialise avec des valeurs par defaut vides. Le risque est theorique et lie a un eventuel changement de comportement de Qwik concernant la serialisation des stores mutes hors closures $().archiveInterval (qui est bien nettoye dans le cleanup) et n'ont pas examine le searchTimerRef separement.searchTimerRef (ligne 77) est un useSignal(0) utilise comme timer ID. Il est clear dans handleSearch (ligne 327) mais JAMAIS dans la closure cleanup() (lignes 158-163). Le cleanup ne contient que clearInterval(archiveInterval) et les removeEventListener. Si l'utilisateur tape dans la recherche puis quitte la page, le callback setTimeout executera sur un composant demonte.role="dialog", aria-modal="true", pas de trap de focus, pas de fermeture par Escape.title (lu inconsistement par les lecteurs d'ecran), pas aria-label (le standard).<div onClick$> non focusables au clavier.aria-live ni de role="status".document.dispatchEvent sans aucun typage TypeScript sur le detail. Un changement de nom ou de structure casse silencieusement la communication.event.data?.title mais n'ont pas de champ datagetEventSummary() pour les types de conversation.ConversationCreatedEvent, ConversationSetEvent, etc. dans types.ts (lignes 313-335) ont des champs top-level (title, summary, conversationId) mais PAS de champ data. Le code EventCard.tsx:130-134 accede a event.data?.title qui retourne toujours undefined. En pratique, ces events sont filtres et ne s'affichent jamais dans le chat, mais le code est incorrect.originalMessage jamais emis par le backend pour objective_completed/failedobjective_completed et objective_failed ne contiennent pas originalMessage. Le code EventCard.tsx:319 verifie event.data?.originalMessage qui est toujours undefined — les boutons "Relancer" et "Modifier" ne s'affichent JAMAIS./ws/cli au lieu de /ws/webuiconf.prod.gouroubleu.yml ni compare avec le backend./api/status ne verifie pas l'API key — validation login toujours true/api/status cote backend.fetch('/api/status', { headers: { 'X-API-Key': key } }) pour verifier si une cle est valide. Si res.ok alors redirect vers /. Mais /api/status retourne toujours 200 (endpoint de health check). Une cle invalide sera donc acceptee, l'utilisateur sera redirige vers /, puis le WS echouera avec auth_error, renvoyant vers /login — creant un flash d'ecran inutile.javascript: URI| Pass | Severite | Justification |
|---|---|---|
| Pass 1 (F11) | FAIBLE | "les navigateurs modernes bloquent javascript: dans les liens" |
| Pass 2 (2.1) | BUG CERTAIN | XSS exploitable |
| Pass 3 (F-06) | BUG PROBABLE | XSS exploitable |
Resolution : Pass 2 a raison. Les navigateurs modernes ne bloquent PAS javascript: dans les <a href> cliques par l'utilisateur. Ils bloquent javascript: tape dans la barre d'adresse. Un lien <a href="javascript:alert(1)"> clique par l'utilisateur executera le JavaScript dans tous les navigateurs majeurs. L'attribut target="_blank" ne protege pas non plus car javascript: est execute dans le contexte de la page origine, pas dans un nouvel onglet (le navigateur ne cree pas de nouvel onglet pour javascript:). Severite retenue : BUG CERTAIN.
| Pass | Description |
|---|---|
| Pass 1 (F01) | "Apres le premier toggle, boucle infinie de requetes toutes les 500ms" |
| Pass 2 (1.4) | "Apres le premier toggle, continue d'appeler loadConversations() indefiniment" |
| Pass 3 (F-09) | Titre : "ne fonctionne jamais". Corps : confirme la boucle infinie. |
Resolution : Pass 1 et Pass 2 sont precis. Le titre de Pass 3 est trompeur, mais le corps de son analyse arrive a la meme conclusion : apres le premier toggle, le setInterval appelle loadConversations() toutes les 500ms indefiniment. Le titre "ne fonctionne jamais" est incorrect — le mecanisme "fonctionne" techniquement (il detecte bien le changement), mais il ne s'arrete jamais. Les trois passes s'accordent sur le comportement reel. Conclusion : boucle infinie apres le premier toggle, pas "ne marche pas".
& dans les URLs — bug ou pas ?| Pass | Verdict |
|---|---|
| Pass 1 (F04) | "pas de bug, les navigateurs decodent & dans les href" |
| Pass 3 (F-07) | "BUG PROBABLE — les URLs sont corrompues" |
Resolution : Pass 1 a raison pour le HTML. Dans un attribut HTML href, les entites HTML sont decodees par le parseur HTML avant l'envoi de la requete. Donc <a href="https://example.com?a=1&b=2"> fonctionne correctement — le navigateur enverra la requete vers https://example.com?a=1&b=2. Cependant, Pass 3 a un point partiel : si le HTML est insere via dangerouslySetInnerHTML (ce qui est le cas), le navigateur decode les entites comme pour du HTML standard. Les liens fonctionnent correctement. C'est un FAUX POSITIF de Pass 3. Voir section "Faux positifs".
event est une prop, pas un store. Les acces sont en lecture seule dans le render.useNavigate() retourne une QRL serialisable par Qwik. Le pattern est correct.component$() est un pattern Qwik valide.& en & dans les attributs href avant d'envoyer la requete. Les liens avec des query params fonctionnent correctement meme avec & dans le HTML. Teste et confirme par le comportement standard de tous les navigateurs. Pass 1 (F04) avait correctement identifie ce point.| # | Finding | Action | Effort | Priorite |
|---|---|---|---|---|
| 1 | C-02 | XSS : filtrer les protocoles dans le renderer Markdown — Ajouter une validation dans la regex des liens pour n'accepter que http:, https:, mailto: et /. Rejeter javascript:, data:, vbscript:. |
5 min | CRITIQUE |
| 2 | C-01 | Supprimer le setInterval archiveInterval — Le handler onClick$ du bouton "Voir archivees" fait deja le fetch. L'interval est inutile et dangereux. | 2 min | CRITIQUE |
| 3 | C-03 | Remplacer window.location.href par useNavigate() dans retryObjective (history/index.tsx). Stocker le message dans localStorage et l'envoyer apres la navigation, ou envoyer via le WS puis naviguer. | 15 min | HAUTE |
| 4 | C-04 | Remplacer setTimeout(500) par un handshake — ChatMessages emet ulias:chat-ready dans son useVisibleTask$, le layout attend cet event avant d'envoyer l'historique. |
20 min | HAUTE |
| 5 | U-01 | Ajouter clearTimeout(searchTimerRef.value) dans le cleanup de ConversationSidebar. | 1 min | HAUTE |
| 6 | C-07 | Verifier res.ok avant de modifier l'etat local dans deleteConversation, archiveConversation, togglePin, saveRename, et deleteObjective. | 15 min | MOYENNE |
| 7 | C-08 | Ajouter confirm() avant les suppressions dans deleteConversation et deleteObjective. | 5 min | MOYENNE |
| 8 | C-11 | Completer les types TypeScript dans types.ts pour correspondre aux events reellement emis par le backend. | 30 min | MOYENNE |
| 9 | C-05 | Ajouter le header X-API-Key dans teams/index.tsx et proteger l'endpoint cote backend. | 5 min | MOYENNE |
| 10 | U-10 | Utiliser un endpoint authentifie pour la validation de la cle au login — remplacer /api/status par /api/profile dans login/index.tsx. |
5 min | MOYENNE |