67 lines
5.5 KiB
Markdown
67 lines
5.5 KiB
Markdown
# SEC-04 — Un membre peut marquer toutes les notifications comme lues
|
||
|
||
**Gravité historique : Moyenne.** Classification : CWE-639.
|
||
|
||
**État au 14 septembre 2026 :** Corrigé dans le code suivi. Le service valide les deux UUID et exige `userId`. Le repository utilise exclusivement `{ id, user_id: userId }`. REST et WebSocket transmettent l'identité authentifiée.
|
||
|
||
## Executive Summary
|
||
|
||
Tout utilisateur autorisé à ouvrir un WebSocket pouvait envoyer `mark_as_read`. Un identifiant de notification d'un autre utilisateur ou un objet JSON à la place de l'identifiant franchissait l'interface sans validation effective.
|
||
|
||
Cette fiche repose sur la relecture du code historique accessible et du correctif courant, ainsi que des preuves documentées lors des passes précédentes. Elle ne constate aucun incident réel. La plus ancienne version ici vérifiée est le snapshot `8446f87` ; les corrections historiques figurent dans `c09b8be`. Aucun tag local ni version déployée n'a permis d'établir une première release vulnérable ou corrigée. Voir [METHODOLOGIE.md](../METHODOLOGIE.md).
|
||
|
||
## Background
|
||
|
||
L’état de lecture appartient au destinataire de la notification. L’identifiant externe doit rester un UUID ; aucun objet fourni par le client ne doit devenir un prédicat de sélection ORM.
|
||
|
||
## Vulnerability Details
|
||
|
||
Le type TypeScript `{ notificationId: string }` ne valide pas un message réseau. Le gateway transmet `data.notificationId` au service sans l'identité du destinataire. Le repository appelle ensuite `ormRepository.update(id, ...)`. Dans TypeORM, un objet non vide peut représenter un prédicat de mise à jour : `{read:false}` ne désigne plus une notification, mais les lignes non lues. Il ne s'agit pas d'une injection de texte SQL ; l'API de sélection du repository accepte une forme trop large. L'endpoint REST avait un contrôle de propriétaire, ce qui ne protégeait pas le point d'entrée WebSocket.
|
||
|
||
Extrait historique vérifié, `apps/backend/src/application/gateways/notifications.gateway.ts`, lignes 117–124 du snapshot préaudit :
|
||
|
||
```typescript
|
||
) {
|
||
try {
|
||
const userId = client.data.userId;
|
||
await this.notificationService.markAsRead(data.notificationId);
|
||
|
||
// Send updated unread count
|
||
const unreadCount = await this.notificationService.getUnreadCount(userId);
|
||
this.emitToUser(userId, 'unread_count', { count: unreadCount });
|
||
```
|
||
|
||
Les numéros ci-dessus décrivent le snapshot ancien ; les liens suivants ouvrent les fichiers courants, où les lignes peuvent avoir changé.
|
||
|
||
Sources à examiner ensemble :
|
||
|
||
- [apps/backend/src/application/gateways/notifications.gateway.ts](../../apps/backend/src/application/gateways/notifications.gateway.ts)
|
||
- [apps/backend/src/application/services/notification.service.ts](../../apps/backend/src/application/services/notification.service.ts)
|
||
- [apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-notification.repository.ts](../../apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-notification.repository.ts)
|
||
|
||
La dépendance locale relue est **TypeORM 0.3.27**. `entity-manager/EntityManager.js`, méthode `update`, rejette les critères vides, utilise `whereInIds` pour les primitives et `.where(criteria)` pour les autres formes. Un objet non vide tel que `{read:false}` n’est donc pas protégé par le rejet des critères vides. Ce détail a été vérifié dans le code installé ; aucune requête destructive n’a été exécutée.
|
||
|
||
## Exploitability Analysis
|
||
|
||
La primitive étroite est une modification de l'état lu/non lu hors du compte appelant ; avec un critère objet, elle peut concerner plusieurs organisations. Aucun contenu de notification n'est exfiltré par cette opération. La portée globale est établie par le chemin statique de critères TypeORM, pas par une mise à jour réellement exécutée contre PostgreSQL en production.
|
||
|
||
Le code applicatif établit le chemin décrit, mais ne renseigne pas les protections effectives d'un déploiement donné, sa version en ligne ou les accès déjà exercés par un attaquant. La gravité ci-dessus est celle du mécanisme vulnérable avant correction ; elle n'est pas un score de risque résiduel calculé pour la production.
|
||
|
||
## Proof of Concept
|
||
|
||
Tests de non-régression existants, inspectables dans le dépôt :
|
||
|
||
- [apps/backend/src/application/services/notification.security.spec.ts](../../apps/backend/src/application/services/notification.security.spec.ts)
|
||
|
||
Ils testent les refus et les usages autorisés avec des données locales. Ils ne prouvent pas qu'une ancienne exploitation a eu lieu en production. Les commandes et résultats consolidés sont dans [VALIDATION.md](../VALIDATION.md) ; aucun nouvel exploit n'a été exécuté pendant la rédaction.
|
||
|
||
## Remediation
|
||
|
||
Corrigé dans le code suivi. Le service valide les deux UUID et exige `userId`. Le repository utilise exclusivement `{ id, user_id: userId }`. REST et WebSocket transmettent l'identité authentifiée.
|
||
|
||
Pour solder le constat en exploitation, vérifier le comportement sur la version effectivement déployée et conserver les contrôles légitimes décrits. La présente passe ajoute uniquement de la documentation ; les correctifs mentionnés existaient avant sa rédaction.
|
||
|
||
## Summary
|
||
|
||
Un membre peut marquer toutes les notifications comme lues est un constat historique de l'audit, à lire avec son état courant ci-dessus. La preuve porte sur le mécanisme et les contrôles cités ; elle ne constitue ni une attestation d'exploitation réelle ni une certification exhaustive du projet.
|