From 05b2002d025d16b26f3074d5d945d54b0a122684 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 14 Aug 2026 18:55:47 +0000 Subject: [PATCH] =?UTF-8?q?Prot=C3=A8ge=20les=20=C3=A9critures=20contre=20?= =?UTF-8?q?les=20requ=C3=AAtes=20forg=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_new/__init__.py | 4 --- app_new/config.py | 4 ++- app_new/equipments/main.py | 2 +- app_new/interventions/templates/detail.html | 8 ++--- app_new/outlook/sync.py | 19 ++++++----- .../outlook/templates/outlook/account.html | 4 +-- .../outlook/templates/outlook/configurer.html | 4 +-- .../outlook/templates/outlook/dashboard.html | 4 +-- app_new/outlook/templates/outlook/folder.html | 4 +-- app_new/outlook/templates/outlook/latest.html | 4 +-- app_new/outlook/templates/outlook/mail.html | 4 +-- app_new/outlook/templates/outlook/mails.html | 4 +-- app_new/pronote/routes.py | 12 ++----- app_new/pronote/templates/pronote/index.html | 7 ++-- .../templates/pronote/planning_index.html | 12 ++++--- app_new/templates/base.html | 34 ++++++++++++++++++- app_new/templates/interventions/detail.html | 8 ++--- app_new/templates/outlook/configurer.html | 4 +-- app_new/templates/outlook/dashboard.html | 4 +-- app_new/templates/outlook/mails.html | 4 +-- app_new/templates/pronote/planning_index.html | 10 +++--- app_new/templates/pronote/rooms.html | 8 ++--- tests/integration/test_request_security.py | 25 ++++++++++++++ 23 files changed, 123 insertions(+), 70 deletions(-) create mode 100644 tests/integration/test_request_security.py diff --git a/app_new/__init__.py b/app_new/__init__.py index 500e76c..0b17f67 100644 --- a/app_new/__init__.py +++ b/app_new/__init__.py @@ -134,7 +134,6 @@ def create_app(config_name='default'): # Blueprints des intégrations from .pronote.routes import pronote_bp - csrf.exempt(pronote_bp) app.register_blueprint(pronote_bp, url_prefix='/pronote') from .ent.routes import ent_bp @@ -172,7 +171,6 @@ def create_app(config_name='default'): # Notifications temps reel from .notifications.routes import notifications_bp app.register_blueprint(notifications_bp) - csrf.exempt(notifications_bp) # Exports PDF/CSV from .exports.routes import exports_bp @@ -181,7 +179,6 @@ def create_app(config_name='default'): # Messagerie unifiee from .messagerie.routes import messagerie_bp app.register_blueprint(messagerie_bp) - csrf.exempt(messagerie_bp) # Contrats d'entreprise from .contracts.models import Contract @@ -199,7 +196,6 @@ def create_app(config_name='default'): # Planificateur automatique from .scheduler.routes import scheduler_bp app.register_blueprint(scheduler_bp) - csrf.exempt(scheduler_bp) # Filtres Jinja2 pour les templates from .utils import format_status, status_badge_class, priority_badge_class, bootstrap_color_to_hex, bootstrap_text_color diff --git a/app_new/config.py b/app_new/config.py index 9ecc6ff..b77f562 100644 --- a/app_new/config.py +++ b/app_new/config.py @@ -49,7 +49,9 @@ class Config: class DevelopmentConfig(Config): """Configuration développement.""" DEBUG = True - WTF_CSRF_ENABLED = False # Désactivé pour développement + # La machine de développement est accessible à distance : les protections + # navigateur doivent y être identiques à celles de la production. + WTF_CSRF_ENABLED = True basedir = os.path.abspath(os.path.dirname(__file__)) # MariaDB obligatoire SQLALCHEMY_DATABASE_URI = os.environ.get( diff --git a/app_new/equipments/main.py b/app_new/equipments/main.py index 11467a1..7fa49d5 100644 --- a/app_new/equipments/main.py +++ b/app_new/equipments/main.py @@ -421,7 +421,7 @@ def create_category(): return render_template('equipments/category_form.html', title='Nouvelle catégorie') -@main_bp.route('//status/') +@main_bp.route('//status/', methods=['POST']) @login_required def change_status(id, status): """Changer le statut d'un équipement.""" diff --git a/app_new/interventions/templates/detail.html b/app_new/interventions/templates/detail.html index 90c7d86..3d309db 100644 --- a/app_new/interventions/templates/detail.html +++ b/app_new/interventions/templates/detail.html @@ -258,9 +258,9 @@ Statut {% endif %} @@ -542,4 +542,4 @@ .bg-orange { background-color: #fd7e14 !important; } .avatar-placeholder { font-weight: 600; } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/outlook/sync.py b/app_new/outlook/sync.py index e0e6bba..6f13817 100644 --- a/app_new/outlook/sync.py +++ b/app_new/outlook/sync.py @@ -11,7 +11,7 @@ import tempfile sync_bp = Blueprint('outlook_sync', __name__, template_folder='templates') -@sync_bp.route('/api/sync-folders/') +@sync_bp.route('/api/sync-folders/', methods=['POST']) @login_required def api_sync_folders(account_id): """Synchronise les dossiers Outlook (avec sous-dossiers récursifs).""" @@ -19,7 +19,7 @@ def api_sync_folders(account_id): from datetime import datetime, timezone account = OutlookAccount.query.get(account_id) - if not account or (account.user_id != current_user.id and current_user.role != 'admin'): + if not account or (account.user_id != current_user.id and not current_user.is_admin()): return jsonify({'error': 'Compte non trouvé'}), 404 access_token, error = get_access_token(account) @@ -122,7 +122,8 @@ def api_sync_folders(account_id): return jsonify({'error': f'Erreur: {str(e)}'}), 500 -@sync_bp.route('/api/sync-mails//') +@sync_bp.route('/api/sync-mails//', methods=['POST']) +@sync_bp.route('/api/sync-mails/', defaults={'folder_id': 'inbox'}, methods=['POST']) @login_required def api_sync_mails(account_id, folder_id='inbox'): """Synchronise les mails d'un dossier avec pagination complète.""" @@ -130,7 +131,7 @@ def api_sync_mails(account_id, folder_id='inbox'): from datetime import datetime, timezone account = OutlookAccount.query.get(account_id) - if not account or (account.user_id != current_user.id and current_user.role != 'admin'): + if not account or (account.user_id != current_user.id and not current_user.is_admin()): return jsonify({'error': 'Compte non trouvé'}), 404 access_token, error = get_access_token(account) @@ -144,7 +145,7 @@ def api_sync_mails(account_id, folder_id='inbox'): # Trouver le dossier folder = None if folder_id != 'inbox': - folder = OutlookFolder.query.get(folder_id) + folder = OutlookFolder.query.filter_by(id=folder_id, account_id=account_id).first() else: # Chercher par folder_type ou par nom (Boîte de réception) folder = OutlookFolder.query.filter_by(account_id=account_id, folder_type='inbox').first() @@ -251,7 +252,7 @@ def api_sync_mails(account_id, folder_id='inbox'): # ─── Pièces jointes ───────────────────────────────────────────────────────── -@sync_bp.route('/api/sync-attachments/') +@sync_bp.route('/api/sync-attachments/', methods=['POST']) @login_required def api_sync_attachments(mail_id): """Synchronise les pièces jointes d'un email.""" @@ -259,6 +260,8 @@ def api_sync_attachments(mail_id): mail = OutlookMail.query.get_or_404(mail_id) account = OutlookAccount.query.get_or_404(mail.account_id) + if account.user_id != current_user.id and not current_user.is_admin(): + return jsonify({'error': 'Message non trouvé'}), 404 access_token, error = get_access_token(account) if error: @@ -317,6 +320,8 @@ def download_attachment(att_id): attachment = OutlookAttachment.query.get_or_404(att_id) mail = OutlookMail.query.get_or_404(attachment.mail_id) account = OutlookAccount.query.get_or_404(mail.account_id) + if account.user_id != current_user.id and not current_user.is_admin(): + return jsonify({'error': 'Pièce jointe non trouvée'}), 404 access_token, error = get_access_token(account) if error: @@ -349,5 +354,3 @@ def download_attachment(att_id): except Exception as e: flash(f'Erreur: {str(e)}', 'danger') return redirect(url_for('outlook_pages.mail_view', account_id=account.id, mail_id=mail.id)) - - diff --git a/app_new/outlook/templates/outlook/account.html b/app_new/outlook/templates/outlook/account.html index 6cc9563..d31f9bb 100644 --- a/app_new/outlook/templates/outlook/account.html +++ b/app_new/outlook/templates/outlook/account.html @@ -189,7 +189,7 @@ function syncFolders() { const statusDiv = document.getElementById('sync-status'); statusDiv.classList.remove('d-none'); - fetch('/outlook/api/sync-folders/{{ account.id }}') + fetch('/outlook/api/sync-folders/{{ account.id }}', {method: 'POST'}) .then(response => response.json()) .then(data => { statusDiv.classList.add('d-none'); @@ -205,4 +205,4 @@ function syncFolders() { }); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/outlook/templates/outlook/configurer.html b/app_new/outlook/templates/outlook/configurer.html index a061009..f282263 100644 --- a/app_new/outlook/templates/outlook/configurer.html +++ b/app_new/outlook/templates/outlook/configurer.html @@ -332,7 +332,7 @@ function testConnection(accountId) { function syncMails(accountId) { if (confirm('Synchroniser les derniers mails ?')) { - fetch(`/outlook/api/sync-mails/${accountId}`) + fetch(`/outlook/api/sync-mails/${accountId}`, {method: 'POST'}) .then(response => response.json()) .then(data => { if (data.success) { @@ -383,4 +383,4 @@ function showError(message) { document.getElementById('authError').style.display = 'block'; } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/outlook/templates/outlook/dashboard.html b/app_new/outlook/templates/outlook/dashboard.html index 53d81a8..6510f83 100644 --- a/app_new/outlook/templates/outlook/dashboard.html +++ b/app_new/outlook/templates/outlook/dashboard.html @@ -245,7 +245,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/outlook/templates/outlook/folder.html b/app_new/outlook/templates/outlook/folder.html index 540c35f..969b230 100644 --- a/app_new/outlook/templates/outlook/folder.html +++ b/app_new/outlook/templates/outlook/folder.html @@ -100,7 +100,7 @@ function syncMails(maxMessages) { status.style.display = 'block'; status.innerHTML = ' Récupération des emails...'; - fetch('/outlook/api/sync-mails/{{ account.id }}/{{ folder.folder_id }}?max=' + maxMessages) + fetch('/outlook/api/sync-mails/{{ account.id }}/{{ folder.folder_id }}?max=' + maxMessages, {method: 'POST'}) .then(response => response.json()) .then(data => { if (data.success) { @@ -126,4 +126,4 @@ function syncMails(maxMessages) { }); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/outlook/templates/outlook/latest.html b/app_new/outlook/templates/outlook/latest.html index 3437d02..2a42e48 100644 --- a/app_new/outlook/templates/outlook/latest.html +++ b/app_new/outlook/templates/outlook/latest.html @@ -54,7 +54,7 @@ function syncMails() { status.style.display = 'block'; status.innerHTML = 'Synchronisation en cours...'; - fetch('/outlook/api/sync-mails/{{ account.id }}') + fetch('/outlook/api/sync-mails/{{ account.id }}', {method: 'POST'}) .then(response => response.json()) .then(data => { if (data.success) { @@ -76,4 +76,4 @@ function syncMails() { }); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/outlook/templates/outlook/mail.html b/app_new/outlook/templates/outlook/mail.html index 9659c67..8f7389d 100644 --- a/app_new/outlook/templates/outlook/mail.html +++ b/app_new/outlook/templates/outlook/mail.html @@ -326,7 +326,7 @@ function syncAttachments() { btn.disabled = true; btn.innerHTML = ' Chargement...'; - fetch('/outlook/api/sync-attachments/{{ mail.id }}') + fetch('/outlook/api/sync-attachments/{{ mail.id }}', {method: 'POST'}) .then(response => response.json()) .then(data => { if (data.success) { @@ -503,4 +503,4 @@ function loadContextStatus() { }); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/outlook/templates/outlook/mails.html b/app_new/outlook/templates/outlook/mails.html index c388528..611bc43 100644 --- a/app_new/outlook/templates/outlook/mails.html +++ b/app_new/outlook/templates/outlook/mails.html @@ -201,7 +201,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/pronote/routes.py b/app_new/pronote/routes.py index a1afcc7..b9e262a 100644 --- a/app_new/pronote/routes.py +++ b/app_new/pronote/routes.py @@ -5,7 +5,6 @@ Intégration Pronote import logging from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify from flask_login import login_required, current_user -from flask_wtf.csrf import CSRFProtect from app_new.extensions import db, csrf from ..pronote.models import PronoteSession from ..core.models.college import Room, RoomSchedule @@ -30,14 +29,7 @@ def index(): @login_required def connect(): """Connexion à Pronote via QR Code.""" - from flask_wtf.csrf import validate_csrf if request.method == 'POST': - # Validate CSRF token manually - try: - validate_csrf(request.form.get('csrf_token')) - except Exception: - # If CSRF fails, try to continue anyway (for testing) - pass qr_code = request.form.get('qr_code', '').strip() pin = request.form.get('pin', '').strip() account_pin = request.form.get('account_pin', '').strip() or None @@ -97,7 +89,7 @@ def rooms(): return render_template('pronote/rooms.html', rooms=rooms_list) -@pronote_bp.route('/sync-salles') +@pronote_bp.route('/sync-salles', methods=['POST']) @login_required def sync_salles(): """Synchroniser les salles depuis Pronote.""" @@ -207,7 +199,7 @@ def import_room_planning(room_id): return redirect(url_for('pronote.room_planning', room_id=room_id)) -@pronote_bp.route('/disconnect') +@pronote_bp.route('/disconnect', methods=['POST']) @login_required def disconnect(): """Déconnexion de Pronote.""" diff --git a/app_new/pronote/templates/pronote/index.html b/app_new/pronote/templates/pronote/index.html index 1ec62b3..6ab51a5 100644 --- a/app_new/pronote/templates/pronote/index.html +++ b/app_new/pronote/templates/pronote/index.html @@ -19,10 +19,11 @@ Connecter à Pronote - +
+
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/pronote/templates/pronote/planning_index.html b/app_new/pronote/templates/pronote/planning_index.html index 19d3815..1ada9c7 100644 --- a/app_new/pronote/templates/pronote/planning_index.html +++ b/app_new/pronote/templates/pronote/planning_index.html @@ -10,9 +10,10 @@ Connexion Pronote - +
+
@@ -53,9 +54,10 @@
Aucune salle d'enseignement disponible.
{% endif %} @@ -93,4 +95,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/base.html b/app_new/templates/base.html index 7b74575..5db7eac 100644 --- a/app_new/templates/base.html +++ b/app_new/templates/base.html @@ -3,6 +3,7 @@ + {% block title %}GMAO Collège{% endblock %} @@ -239,7 +240,7 @@
  • Personnels ENT
  • PRONOTE (QR)
  • -
  • PRONOTE (sync salles)
  • +
  • PRONOTE (plannings)
  • Téléphone P520
  • @@ -320,6 +321,37 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/outlook/dashboard.html b/app_new/templates/outlook/dashboard.html index f93df43..6034826 100644 --- a/app_new/templates/outlook/dashboard.html +++ b/app_new/templates/outlook/dashboard.html @@ -245,7 +245,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/outlook/mails.html b/app_new/templates/outlook/mails.html index 58a9253..79b52b3 100644 --- a/app_new/templates/outlook/mails.html +++ b/app_new/templates/outlook/mails.html @@ -201,7 +201,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/pronote/planning_index.html b/app_new/templates/pronote/planning_index.html index 19d3815..015c7ee 100644 --- a/app_new/templates/pronote/planning_index.html +++ b/app_new/templates/pronote/planning_index.html @@ -10,9 +10,9 @@ Connexion Pronote - +
    @@ -53,9 +53,9 @@
    Aucune salle d'enseignement disponible.
    {% endif %} @@ -93,4 +93,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/pronote/rooms.html b/app_new/templates/pronote/rooms.html index 27affa5..522af88 100644 --- a/app_new/templates/pronote/rooms.html +++ b/app_new/templates/pronote/rooms.html @@ -31,9 +31,9 @@ {% else %}
    Aucune salle importée depuis Pronote. - - Synchroniser les salles - +
    + +
    {% endif %} @@ -43,4 +43,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tests/integration/test_request_security.py b/tests/integration/test_request_security.py new file mode 100644 index 0000000..e83ec5b --- /dev/null +++ b/tests/integration/test_request_security.py @@ -0,0 +1,25 @@ +"""Régressions sur les protections des requêtes qui écrivent.""" +from app_new.config import DevelopmentConfig + + +def test_csrf_is_enabled_in_remote_development(): + assert DevelopmentConfig.WTF_CSRF_ENABLED is True + + +def test_login_post_without_csrf_is_rejected_when_enabled(client, app): + previous = app.config["WTF_CSRF_ENABLED"] + app.config["WTF_CSRF_ENABLED"] = True + try: + response = client.post("/auth/login", data={"username": "inconnu", "password": "inconnu"}) + assert response.status_code == 400 + finally: + app.config["WTF_CSRF_ENABLED"] = previous + + +def test_mutating_routes_reject_get(client): + assert client.get("/equipments/1/status/hs").status_code == 405 + assert client.get("/pronote/sync-salles").status_code == 405 + assert client.get("/pronote/disconnect").status_code == 405 + assert client.get("/outlook/api/sync-folders/1").status_code == 405 + assert client.get("/outlook/api/sync-mails/1").status_code == 405 + assert client.get("/outlook/api/sync-attachments/inconnu").status_code == 405