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]>
This commit is contained in:
+28
-15
@@ -30,7 +30,13 @@ with app.app_context():
|
||||
db.create_all()
|
||||
```
|
||||
|
||||
Ni `Flask-Migrate` ni `alembic` ne figurent dans `requirements.txt`, et il n'existe pas de dossier `migrations/`.
|
||||
Ni `Flask-Migrate` ni `alembic` ne figurent dans `requirements.txt`.
|
||||
|
||||
Un dossier `migrations/` existe désormais, mais il ne contient **pas** un outillage de migration : c'est un script unique écrit à la main, `migrations/add_tryout_coaches.py`, qui exécute du DDL en SQL brut et vérifie lui-même son idempotence en interrogeant `information_schema`.
|
||||
|
||||
Le script est correct dans ce qu'il fait — le SQL n'interpole aucune entrée utilisateur, la création de table est conditionnelle, et la migration de données vérifie qu'elle n'a pas déjà eu lieu. Il illustre surtout le manque : chaque évolution de schéma exige de réécrire à la main ce qu'Alembic fournit. Il n'y a ni table de version, ni ordonnancement, ni migration inverse, ni moyen de savoir quelles migrations ont été appliquées sur quelle base — l'idempotence est réimplémentée au cas par cas.
|
||||
|
||||
Il hérite en outre des effets de bord de la fabrique : `create_app()` à la ligne 15 déclenche `db.create_all()` et **démarre le bot Discord** ([STD-02](#std-02--), [STD-03](#std-03--)). Appliquer une migration ouvre une connexion Discord.
|
||||
|
||||
`db.create_all()` **ne crée que les tables absentes**. Il ne modifie jamais une table existante : ajouter une colonne, changer un type, ajouter une contrainte d'unicité ou une clé étrangère `ON DELETE CASCADE` n'a aucun effet sur une base déjà initialisée.
|
||||
|
||||
@@ -78,11 +84,17 @@ et retirer `db.create_all()` de la fabrique — les migrations s'appliquent lors
|
||||
|
||||
| Ligne | Effet de bord |
|
||||
|---|---|
|
||||
| `:351` | `db.create_all()` — écriture dans le schéma |
|
||||
| `:354-356` | Insertion des données de démonstration si la table est vide |
|
||||
| `:359-361` | Démarrage d'un thread portant un client Discord |
|
||||
| `:349` | `db.create_all()` — écriture dans le schéma |
|
||||
| `:353-355` | Démarrage d'un thread portant un client Discord |
|
||||
| `logging_config.py:66-67` | `os.makedirs('logs')` dans le répertoire de travail courant |
|
||||
|
||||
L'insertion automatique de données de démonstration a été retirée (voir [`00-provenance.md`](00-provenance.md)) — c'était le plus grave de ces effets de bord. Les deux autres subsistent, et leurs conséquences se voient désormais très concrètement dans les deux scripts utilitaires ajoutés depuis :
|
||||
|
||||
- `clear_db.py:54` appelle `create_app()` pour vider la base — **et démarre donc un bot Discord** au passage ;
|
||||
- `migrations/add_tryout_coaches.py:15` fait de même pour appliquer une migration de schéma.
|
||||
|
||||
Aucun de ces deux scripts n'a besoin de Discord ; tous deux l'obtiennent, parce qu'il n'existe pas de moyen de construire l'application sans.
|
||||
|
||||
Le motif de fabrique d'application (*application factory*) existe précisément pour permettre de construire plusieurs instances configurées différemment — c'est ce qui rend une application Flask testable. Ici, chaque appel à `create_app()` écrit dans la base et ouvre une connexion Discord.
|
||||
|
||||
**Impact.**
|
||||
@@ -191,7 +203,9 @@ app.config['SESSION_COOKIE_SECURE'] = os.getenv('SESSION_COOKIE_SECURE', 'true')
|
||||
...
|
||||
```
|
||||
|
||||
D'autres réglages sont lus directement via `os.getenv()` au fil du code, hors de `app.config` : `FORCE_HTTPS` (`app.py:183`), `LOG_LEVEL` et `FLASK_DEBUG` (`logging_config.py:73,133`), `DISCORD_WEBHOOK_URL` (`users.py:636`, au moment de l'import), `DISCORD_BOT_TOKEN` (`discord_bot.py:24`). `load_dotenv()` est appelé dans deux modules distincts (`app.py:16`, `discord_bot.py:23`).
|
||||
D'autres réglages sont lus directement via `os.getenv()` au fil du code, hors de `app.config` : `FORCE_HTTPS` (`app.py:183`), `LOG_LEVEL` et `FLASK_DEBUG` (`logging_config.py:73,133`), `DISCORD_WEBHOOK_URL` (`users.py:657`), `DISCORD_BOT_TOKEN` (`discord_bot.py:24`) et, depuis l'ajout de l'OAuth2, `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET` et `DISCORD_REDIRECT_URI` (`auth.py:25-27`). `load_dotenv()` est appelé dans deux modules distincts (`app.py:16`, `discord_bot.py:23`).
|
||||
|
||||
Les trois dernières sont lues **au moment de l'import du module**, comme `DISCORD_WEBHOOK_URL`. Conséquence pratique : elles ne peuvent être ni surchargées en test, ni rechargées sans redémarrer le processus. Et rien ne vérifie leur présence au démarrage — `discord_login` teste `DISCORD_CLIENT_ID` (`auth.py:336`) mais pas les deux autres, si bien qu'un `DISCORD_REDIRECT_URI` manquant produit un `TypeError` à la ligne 346 au lieu d'un message clair (cf. [SEC-20](01-securite.md#sec-20--)).
|
||||
|
||||
Il n'existe **aucune notion d'environnement** : les mêmes valeurs par défaut s'appliquent en développement, en CI et en production. C'est la cause directe de SEC-02 (seed en production) et de SEC-03 (CORS permissif par défaut).
|
||||
|
||||
@@ -238,26 +252,25 @@ Un seul `load_dotenv()`, au point d'entrée. Toute lecture de configuration pass
|
||||
| Fichier | Ligne | Écoute / cible |
|
||||
|---|---|---|
|
||||
| `run.py` | `:26` | `app.run(host='127.0.0.2', port=5000)` |
|
||||
| `app/app.py` | `:377` | `app.run(host='0.0.0.0', port=10000)` |
|
||||
| `app/app.py` | `:371` | `app.run(host='0.0.0.0', port=10000)` |
|
||||
| `wsgi.py` | `:24-26` | `PORT` défaut **10000**, `HOST` défaut `0.0.0.0` |
|
||||
| `app/.env.exemple` | `:25-26` | `HOST=127.0.0.1`, `PORT=5000` |
|
||||
| `app/nginx.conf` | `:122` | `proxy_pass http://0.0.0.0:5000` |
|
||||
| `app/.env.exemple` | `:31-32` | `HOST=127.0.0.1`, `PORT=5000` |
|
||||
| `app/nginx.conf` | `:122` | `proxy_pass http://127.0.0.1:5000` ✅ corrigé |
|
||||
|
||||
Quatre combinaisons différentes pour trois points d'entrée. Points notables :
|
||||
Le `proxy_pass` a été corrigé (commit `10af8c0`) : il visait `0.0.0.0:5000`, ce qui n'a pas de sens comme adresse de destination. Il reste quatre combinaisons différentes pour trois points d'entrée. Points notables :
|
||||
|
||||
- **`127.0.0.2` dans `run.py`** est presque certainement une faute de frappe pour `127.0.0.1`. L'adresse est techniquement valide (tout le bloc `127.0.0.0/8` est en boucle locale sous Linux), mais elle n'est pas joignable sous Windows sans configuration supplémentaire — or le projet cible Windows (Waitress, `nginx.conf` avec des chemins `C:/`).
|
||||
- **`proxy_pass http://0.0.0.0:5000`** : `0.0.0.0` désigne « toutes les interfaces » comme adresse d'écoute, mais n'a pas de sens comme adresse de destination. La cible correcte est `127.0.0.1:5000`.
|
||||
- **`wsgi.py` écoute sur 10000 par défaut, nginx envoie vers 5000.** Sans `PORT=5000` dans l'environnement, le proxy ne trouve pas l'application.
|
||||
- **`127.0.0.2` dans `run.py`** est presque certainement une faute de frappe pour `127.0.0.1`. L'adresse est techniquement valide (tout le bloc `127.0.0.0/8` est en boucle locale sous Linux), mais elle n'est pas joignable sous Windows sans configuration supplémentaire.
|
||||
- **`wsgi.py` écoute sur 10000 par défaut, nginx envoie désormais vers 5000.** Sans `PORT=5000` dans l'environnement, le proxy ne trouve pas l'application. Le docstring du même fichier (`:11`) annonce pourtant *« PORT: Port to listen on (default: 5000) »* — la documentation et le code se contredisent dans le même fichier.
|
||||
- **`HOST` par défaut à `0.0.0.0`** dans `wsgi.py:26`, avec un commentaire annonçant l'inverse. C'est un facteur direct de [SEC-05](01-securite.md#sec-05--).
|
||||
- Le commentaire de `wsgi.py:26` dit *« Bind to localhost by default (Nginx reverse proxy) »* alors que le défaut est `0.0.0.0` — l'intention est documentée, l'implémentation fait l'inverse (cf. SEC-05, dont c'est un facteur aggravant).
|
||||
|
||||
**Impact.** La chaîne nginx → Waitress ne fonctionne pas avec les valeurs par défaut : elle exige des variables d'environnement non documentées dans le README. Un déploiement suivant la documentation aboutit à un 502.
|
||||
|
||||
**Correction.**
|
||||
1. Un seul point d'entrée de production : `wsgi.py`, avec `HOST` par défaut à `127.0.0.1` et `PORT` par défaut à `5000` pour s'aligner sur nginx et sur `.env.exemple`.
|
||||
2. Supprimer le bloc `if __name__ == '__main__'` de `app/app.py:368-377`, qui fait doublon avec `run.py` et diverge de lui.
|
||||
2. Supprimer le bloc `if __name__ == '__main__'` de `app/app.py`, qui fait doublon avec `run.py` et diverge de lui.
|
||||
3. Corriger `run.py:26` en `127.0.0.1` et lui faire lire `HOST`/`PORT`.
|
||||
4. `nginx.conf:122` → `proxy_pass http://127.0.0.1:5000;`.
|
||||
5. Documenter le tableau des ports dans le README.
|
||||
4. Documenter le tableau des ports dans le README.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user