# Audit de sécurité - Routes de réglages

## Date

$(date)

## Résumé exécutif

**Niveau de risque: ÉLEVÉ**

Trois failles critiques de validation détectées qui permettent l'injection de valeurs invalides dans la base de données.

---

## Failles détectées

### 1. ⚠️ CRITIQUE: Pas de validation enum pour `theme`

**Localisation:** `wallet-routes.ts` ligne 306

**Problème:**

```typescript
// Schéma ACTUEL (INSÉCURISÉ)
theme: { type: "string", minLength: 3, maxLength: 20 }
```

**Risque:**

- N'importe quelle chaîne entre 3-20 caractères est acceptée
- Un attaquant peut injecter: `"xss_payload"`, `"'; DROP TABLE--"`, `"../../../"`, etc.
- Les valeurs invalides sont stockées en base de données
- Le client essaiera d'appliquer le thème et pourrait crash ou afficher des erreurs

**Exemple d'attaque:**

```bash
curl -X PUT http://api/settings/123 \
  -H "Content-Type: application/json" \
  -d '{
    "music": true,
    "sfx": true,
    "haptics": true,
    "theme": "'; DROP TABLE users--",
    "animationSpeed": "normal",
    "sfxVolume": 0.7,
    "musicVolume": 0.4
  }'
```

**Valeurs acceptées:**

- `"ocean"`
- `"sunset"`
- `"neon"`

**Solution:**

```typescript
// Schéma SÉCURISÉ
theme: { type: "string", enum: ["ocean", "sunset", "neon"] }
```

---

### 2. ⚠️ CRITIQUE: Pas de validation enum pour `animationSpeed`

**Localisation:** `wallet-routes.ts` ligne 307

**Problème:**

```typescript
// Schéma ACTUEL (INSÉCURISÉ)
animationSpeed: { type: "string", minLength: 3, maxLength: 20 }
```

**Risque:**

- Même vulnérabilité que `theme`
- N'importe quelle chaîne est acceptée
- Les valeurs invalides cassent le gameplay (pas de timing valide)

**Valeurs acceptées:**

- `"slow"`
- `"normal"`
- `"fast"`

**Solution:**

```typescript
// Schéma SÉCURISÉ
animationSpeed: { type: "string", enum: ["slow", "normal", "fast"] }
```

---

### 3. ⚠️ Pas de validation des énumérations côté application

**Localisation:** `wallet-routes.ts` lignes 336-338 (UPDATE SQL)

**Problème:**
Les paramètres sont directement insérés dans le SQL sans validation post-schema:

```typescript
await db.execute(
  "UPDATE user_settings SET ... theme = ?, animation_speed = ? WHERE user_id = ?",
  [body.music, body.sfx, body.haptics, body.theme, body.animationSpeed, ...]
);
```

**Risque:**
Même si Fastify valide le schéma, une validation manuelle améliore la robustesse.

**Solution:**
Ajouter une validation manuelle explicite.

---

## Impact sur la sécurité

### Scénarios d'attaque

#### Scénario 1: Pollution de base de données

```
Attaquant envoie: theme = "malicious_value_xyz"
→ Base de données stocke une valeur invalide
→ Client récupère la valeur et la tente d'appliquer
→ Affichage cassé ou comportement indéfini
→ Logs remplis d'erreurs
```

#### Scénario 2: Contournement de validation côté client

L'application suppose que `theme` et `animationSpeed` sont valides car ils viennent du serveur.

```typescript
// Client-side (main.ts ligne 188):
const userTheme = loadSettings().theme || "ocean";
// Si theme = "xss_attack", le client essaie de l'utiliser!
```

#### Scénario 3: Injection de code via stockage

Bien que l'injection SQL soit protégée par les requêtes paramétrées, les valeurs de chaîne non validées pourraient:

- Déclencher des bugs de rendu
- Causer des erreurs XML/JSON côté client
- Permettre la déni de service (DoS) en remplissant la base de données de chaînes longues

---

## Recommandations

### ✅ Solution immédiate (MUST DO)

Modifier `wallet-routes.ts` lignes 254-351:

```typescript
export const registerSettingsRoutes = (
  app: FastifyInstance,
  ctx: RouteContext,
) => {
  const { db, requireAuth } = ctx;

  // Constantes des valeurs valides
  const VALID_THEMES = ["ocean", "sunset", "neon"] as const;
  const VALID_ANIMATION_SPEEDS = ["slow", "normal", "fast"] as const;

  // GET /settings/:userId
  app.get(
    "/settings/:userId",
    {
      preHandler: requireAuth,
      schema: { params: paramsUserIdSchema },
    },
    async (request, reply) => {
      const userId = Number((request.params as { userId: string }).userId);
      const tokenUserId = request.user?.sub;
      if (!Number.isFinite(userId)) {
        reply.code(400);
        return { error: "userId invalide" };
      }
      if (!tokenUserId || tokenUserId !== userId) {
        reply.code(403);
        return { error: "Acces refuse" };
      }
      const settings = await fetchSettings(db, userId);
      return { settings };
    },
  );

  // PUT /settings/:userId
  app.put(
    "/settings/:userId",
    {
      preHandler: requireAuth,
      schema: {
        params: paramsUserIdSchema,
        body: {
          type: "object",
          required: [
            "music",
            "sfx",
            "haptics",
            "theme",
            "animationSpeed",
            "sfxVolume",
            "musicVolume",
          ],
          additionalProperties: false,
          properties: {
            music: { type: "boolean" },
            sfx: { type: "boolean" },
            haptics: { type: "boolean" },
            // ✅ SÉCURISÉ: Validation enum
            theme: {
              type: "string",
              enum: ["ocean", "sunset", "neon"],
            },
            // ✅ SÉCURISÉ: Validation enum
            animationSpeed: {
              type: "string",
              enum: ["slow", "normal", "fast"],
            },
            sfxVolume: { type: "number", minimum: 0, maximum: 1 },
            musicVolume: { type: "number", minimum: 0, maximum: 1 },
          },
        },
      },
    },
    async (request, reply) => {
      const userId = Number((request.params as { userId: string }).userId);
      const body = request.body as SettingsState | undefined;
      const tokenUserId = request.user?.sub;

      if (!Number.isFinite(userId)) {
        reply.code(400);
        return { error: "userId invalide" };
      }

      if (!tokenUserId || tokenUserId !== userId) {
        reply.code(403);
        return { error: "Acces refuse" };
      }

      if (!body) {
        reply.code(400);
        return { error: "Payload invalide" };
      }

      // ✅ Validation manuelle supplémentaire (defense in depth)
      if (!VALID_THEMES.includes(body.theme as any)) {
        reply.code(400);
        return { error: "theme invalide" };
      }

      if (!VALID_ANIMATION_SPEEDS.includes(body.animationSpeed as any)) {
        reply.code(400);
        return { error: "animationSpeed invalide" };
      }

      await ensureUserSettings(db, userId);
      await db.execute(
        "UPDATE user_settings SET music = ?, sfx = ?, haptics = ?, theme = ?, animation_speed = ?, sfx_volume = ?, music_volume = ? WHERE user_id = ?",
        [
          body.music,
          body.sfx,
          body.haptics,
          body.theme,
          body.animationSpeed,
          body.sfxVolume,
          body.musicVolume,
          userId,
        ],
      );

      await recordAudit(db, {
        userId,
        action: "settings.update",
        ip: request.ip,
        userAgent: request.headers["user-agent"]?.toString() ?? null,
      });

      const settings = await fetchSettings(db, userId);
      return { settings };
    },
  );
};
```

---

## Checklist de sécurité supplémentaire

### Rate limiting

- ✅ Apparemment activé au niveau global (`rateLimit` dans server.ts)
- ⚠️ **VÉRIFIER:** Si des limites spécifiques à `/settings/` sont nécessaires (plus restrictif que la limite globale)

### Authorization

- ✅ Vérification `tokenUserId === userId`
- ✅ Bloquer les non-authentifiés

### SQL Injection

- ✅ Requêtes paramétrées utilisées (`?` placeholders)

### XSS

- ✅ Les valeurs de réglages ne sont pas rendues en HTML

### Autres considérations

- ✅ CORS correctement configuré
- ✅ JWT validé
- ✅ Audit logging enregistré

---

## Impact du fix

| Avant                                                             | Après                                                                  |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `theme: { type: "string", minLength: 3, maxLength: 20 }`          | `theme: { type: "string", enum: ["ocean", "sunset", "neon"] }`         |
| `animationSpeed: { type: "string", minLength: 3, maxLength: 20 }` | `animationSpeed: { type: "string", enum: ["slow", "normal", "fast"] }` |
| Validation manuelle: ❌                                           | Validation manuelle: ✅                                                |
| Valeurs invalides acceptées: ✅                                   | Valeurs invalides acceptées: ❌                                        |

---

## Autres failles identifiées dans le code

### 🟡 Profile routes (profile-routes.ts)

**Status:** ✅ Sécurisé (déjà corrigé)

- Rate limiting: ✅ Implémenté
- HTML/XSS protection: ✅ `hasHtmlOrDangerousChars()`

### 🟡 Client-side validation (action-handler.ts)

**Status:** ✅ Pas de problème majeur

- Validation optimiste côté client: OK pour UX
- Serveur est la source de vérité: ✅

---

## Checklist pour déploiement

- [ ] Modifier `wallet-routes.ts` avec les validations enum
- [ ] Tester avec des valeurs invalides: `"random_theme"`, `"super_fast"`
- [ ] Vérifier que le serveur rejette les demandes invalides (400)
- [ ] Vérifier que les valeurs valides continuent à fonctionner
- [ ] Compiler TypeScript: `npm run build`
- [ ] Tester les réglages dans l'app mobile
- [ ] Commit et push
- [ ] Déployer

---

## Sévérité globale

**ÉLEVÉE** - Les failles permettent le stockage de données non validées dans la base de données, ce qui pourrait causer des bugs imprévisibles, du DoS ou de la corruption de données.

**Temps de correction:** ~10 minutes
**Impact de correction:** Très faible - peu de chance de régression
