Files
team-tryouts/audit/00-provenance.md
T
GGThedandClaude Opus 5 ed586233f6 docs(audit): rebase sur le depot de reference et reverification complete
Le miroir GitHub audite en premiere passe etait en retard de 16 commits
sur git.immortal.host/clubesportsudes/team-tryouts. L'audit est rebase
sur immortal/main @ bb0bc1c et l'ensemble des constats reverifie.

Resolus par l'equipe (archives dans 00-provenance.md) :
- seed automatique en production supprime
- print du token Discord supprime
- proxy_pass nginx corrige vers 127.0.0.1
- dossier supporting_scrits renomme

Nouveaux constats :
- SEC-20 flux OAuth2 Discord sans parametre state (CSRF de liaison)
- SEC-21 le deploiement SFTP pousse .git/ sur le serveur
- SEC-22 clear_db.py destructif sans garde-fou, admin/password en dur
- MNT-16 discord_pending.json versionne

Requalifies :
- SEC-01 secrets retires du fichier mais toujours dans l'historique des
  deux depots, et dans le HEAD du miroir GitHub -> revocation requise
- SEC-05 trusted_proxy='*' + bind 0.0.0.0 rend X-Forwarded-For usurpable,
  ce qui ouvre le rate limiting au lieu de le corriger
- SEC-07 l'OAuth2 ajoute ne contraint pas l'identite : le discord_user_id
  transite par un champ cache du formulaire
- MNT-05 psycopg[binary] non epingle, incompatible avec les URI
  postgresql:// que SQLAlchemy resout vers psycopg2

48 constats. Aucune modification du code applicatif.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 13:17:08 -04:00

150 lines
8.2 KiB
Markdown

# 0 — Provenance et écart entre les dépôts
## Deux dépôts, un seul fait autorité
| Dépôt | Rôle | HEAD `main` |
|---|---|---|
| `git.immortal.host/clubesportsudes/team-tryouts` | **Référence** — c'est celui-ci qui est audité | `bb0bc1c` |
| `github.com/cedrick2711/team-tryouts` | Miroir, **en retard de 16 commits** | `08f02f7` |
Les deux partagent un ancêtre commun (`d6fe505`). Le miroir GitHub porte en outre 2 commits qui ne sont pas sur le dépôt de référence (`0d5a408 fix registration` et son merge `08f02f7`) ; le correctif correspondant existe sur `immortal` sous la forme du commit `7d30aff Corriger erreur d'enregistrement`.
**La première passe de cet audit a été menée sur le miroir GitHub**, avant que la bonne source ne soit connue. L'ensemble des constats a été revérifié contre `immortal/main`. Le présent document liste ce qui a changé.
### Les 16 commits d'écart
```
bb0bc1c ajout de plateforme de base pour les url TRN
f89e4de régler problème avec mise a jour des status de message du discord bot
fcf10bc bug fix: Manager ne pouvait pas voir les tryouts. probleme avec discord bot
68e3da6 fix probleme avec dispos
d25b35c régler erreur 500 sur changement de role par admin
aeba4d7 régler problème de changement de rôle
7bb8022 added a clear_db to start fresh with only an admin
0dd4ecd Erreur de frappe dans un des dossiers
0f7788e Changement du layout de la page d'enregistrement
47d5ec4 added discord oauth2 to get basic user info to complete profile when registering
10af8c0 small change for prod
7d30aff Corriger erreur d'enregistrement
4097416 Update wsgi.py
056cea0 Update .gitea/workflows/git-to-ptero.yaml
d3b5700 Add .gitea/workflows/git-to-ptero.yaml
fd258de Update app/.env.exemple
```
---
## Constats de la première passe devenus caducs
Quatre constats ont été résolus par l'équipe entre les deux points de l'historique. Ils sont conservés ici pour mémoire, et retirés du décompte des documents 01 à 03.
### ✅ Seed automatique en production — résolu
Le bloc de `app/app.py` qui déclenchait le seed quand la table `users` était vide a été supprimé (commit `7bb8022`), et `app/supporting_scrits/seed.py` (460 lignes, comptes `manager1`, `coach1`, `scout1`… tous avec le mot de passe `password`) a été supprimé.
```diff
with app.app_context():
import app.models as models
- from app.models import User
db.create_all()
-
- # Seed database if empty
- if User.query.count() == 0:
- from app.supporting_scrits.seed import seed_database
- seed_database()
```
Un déploiement neuf ne crée donc plus de comptes par défaut.
> **Reste à traiter.** Le script de remplacement `clear_db.py` code toujours en dur `admin` / `password`, et il est bien plus dangereux que l'ancien seed sur un autre plan. Voir [SEC-22](01-securite.md#sec-22--).
>
> **À vérifier en production**, indépendamment du code : les comptes créés par l'ancien seed (`admin`, `manager1`, `manager2`, `coach1`, `coach2`, `coach3`, `scout1`) peuvent toujours exister en base avec le mot de passe `password`. La suppression du script ne supprime pas les comptes qu'il a créés.
### ✅ Token Discord imprimé sur stdout — résolu
La ligne `print(DISCORD_BOT_TOKEN or 'FAILED TO PRINT BOT TOKEN')` de `app/discord_bot.py:25` a été supprimée. Le module ne comporte plus aucun `print()`.
### ✅ `proxy_pass` vers `0.0.0.0` — résolu
`app/nginx.conf:122` : `proxy_pass http://0.0.0.0:5000;``proxy_pass http://127.0.0.1:5000;`
> **Reste à traiter.** L'incohérence de ports subsiste : `wsgi.py:24` utilise toujours `PORT` par défaut à **10000** alors que nginx envoie vers **5000** et que le docstring du même fichier annonce 5000. Voir [STD-06](03-standards-stack.md#std-06--).
### ✅ Faute de frappe `supporting_scrits` — résolu
Le dossier a été renommé `app/supporting_scripts/` (commit `0dd4ecd`).
> **Effet de bord non traité.** Le job CI « Security Scan » invoquait déjà un chemin erroné (`python security_scan.py` à la racine) ; le renommage l'éloigne encore. Voir [MNT-04](02-maintenabilite.md#mnt-04--).
---
## Constats aggravés ou requalifiés
### ⚠️ Secrets exposés — le nettoyage n'a pas révoqué
`app/.env.exemple` a été nettoyé (commit `fd258de`) : les valeurs réelles ont été remplacées par des marqueurs (`flask_app_secret_key`, `my_discord_bot_token`, `URI_vers_db_posgres`).
**Mais les secrets restent intégralement récupérables.** Vérification par recherche dans l'historique complet des deux dépôts :
```
$ git log --all --oneline -S "MTUyNzY3ODU3NjUyNTA1NDEyNQ"
fd258de Update app/.env.exemple ← retrait
2d3721b Ajout d'un .env.exemple pour simplifier la collaboration ← introduction
```
Le blob contenant le token Discord, le mot de passe PostgreSQL et la `SECRET_KEY` est toujours atteignable par `git show 2d3721b:app/.env.exemple` sur **les deux dépôts**. Il est de surcroît **toujours dans le HEAD du miroir GitHub**, donc visible par simple navigation dans l'interface web.
Le constat reste donc critique et sa correction inchangée : **révoquer, puis purger**. Voir [SEC-01](01-securite.md#sec-01--).
### ⚠️ En-têtes de proxy — le correctif a inversé le risque
`wsgi.py` a reçu un bloc de configuration proxy (commit `4097416`) :
```python
trusted_proxy='*',
trusted_proxy_count=1,
trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto'},
clear_untrusted_proxy_headers=True
```
L'intention est bonne, et `clear_untrusted_proxy_headers=True` est le bon réflexe. Mais `trusted_proxy='*'` signifie « faire confiance aux en-têtes de proxy quelle qu'en soit la provenance », et `HOST` vaut toujours `0.0.0.0` par défaut (`wsgi.py:26`).
Avant ce changement, `request.remote_addr` valait toujours l'IP de nginx : le rate limiting était appliqué à un seau global — gênant, mais fermé. Désormais, un attaquant qui atteint directement le port applicatif contrôle `X-Forwarded-For` et peut donc **présenter une IP différente à chaque requête**, ce qui neutralise le rate limiting et le suivi de tentatives par IP.
Le constat change de nature et reste élevé. Voir [SEC-05](01-securite.md#sec-05--).
### ⚠️ Identifiant Discord — la vérification ajoutée n'en est pas une
Un flux OAuth2 Discord complet a été ajouté à l'inscription (commit `47d5ec4`, `auth.py:326-456`). Il obtient de Discord un identifiant authentifié et le place en session.
Mais cet identifiant est ensuite réinjecté dans le formulaire d'inscription **par un champ caché** :
```html
<!-- register.html:61 -->
<input type="hidden" name="discord_user_id" value="{{ discord_data.id }}">
```
et c'est cette valeur — passée par le client — qui est enregistrée (`auth.py:253`, `:290`). Un utilisateur peut la modifier avant envoi, ou poster directement le formulaire sans jamais passer par Discord.
`RegisterSchema` valide désormais le format de `discord_user_id` (17-20 chiffres, `validators.py:211-215`), ce qui est un progrès, mais ne prouve rien sur la propriété du compte.
La conséquence pratique est inchangée par rapport à la première passe — et le risque de fausse assurance est nouveau, puisque le flux *paraît* vérifié. Voir [SEC-07](01-securite.md#sec-07--).
### ⚠️ Limite d'inscription desserrée
`auth.py:190` : `@limiter.limit("3 per hour")``@limiter.limit("20 per hour")`.
Le CAPTCHA arithmétique étant trivial ([SEC-13](01-securite.md#sec-13--)), le rate limiting était la protection réellement efficace contre la création automatisée de comptes. Passer à 20/heure la divise par un facteur ~7, et [SEC-05](01-securite.md#sec-05--) la rend contournable.
---
## Éléments nouveaux ajoutés à l'audit
| Élément | Constat |
|---|---|
| `auth.py:326-456` — flux OAuth2 Discord | [SEC-20](01-securite.md#sec-20--) absence de paramètre `state` · [SEC-07](01-securite.md#sec-07--) identité non contraignante |
| `.gitea/workflows/git-to-ptero.yaml` | [SEC-21](01-securite.md#sec-21--) déploiement de `.git/` par SFTP |
| `clear_db.py` | [SEC-22](01-securite.md#sec-22--) script destructif sans garde-fou |
| `migrations/add_tryout_coaches.py` | [STD-01](03-standards-stack.md#std-01--) migration manuelle hors outillage |
| `discord_pending.json` | [MNT-16](02-maintenabilite.md#mnt-16--) état d'exécution versionné |