ajouter des sécurité sur les URL, les Roles, les mdp

This commit is contained in:
cedrick2711
2026-07-20 14:57:12 -04:00
parent 482211e6e0
commit 87a84fd43b
20 changed files with 177 additions and 51 deletions
+7
View File
@@ -4,3 +4,10 @@ instance/
documents/
__pycache__/
*.pyc
*.db
team_tryouts.db
.pytest_cache/
.coverage
htmlcov/
.DS_Store
*.log
+26
View File
@@ -1,5 +1,31 @@
### Plateforme centralisée de tryouts
## Security Configuration
### Required Environment Variables
Before deploying, create a `.env` file with the following:
```
# Flask Configuration (REQUIRED)
SECRET_KEY=your-secure-random-secret-key-here
# Production Settings
FLASK_DEBUG=false
FORCE_HTTPS=true
SESSION_COOKIE_SECURE=true
```
### Security Features Implemented
- **Rate Limiting**: Login endpoint limited to 10 requests per minute to prevent brute-force attacks
- **Secure Session Cookies**: HTTPOnly, SameSite=Lax, and Secure flags enabled
- **CSRF Protection**: Enabled by default on all forms
- **HTTPS Enforcement**: Automatic redirect to HTTPS in production
- **Security Headers**: X-Frame-Options, X-Content-Type-Options, Content-Security-Policy, HSTS
- **Open Redirect Prevention**: URL validation on login redirect
- **Authorization Checks**: Proper ownership validation on all sensitive operations
## Discord Integration for One on One Requests
The application supports sending Discord direct messages to coaches when players request One on One sessions.
Binary file not shown.
+39 -6
View File
@@ -5,10 +5,13 @@ the Flask application instance.
"""
import os
from flask import Flask
from extensions import db, login_manager, csrf, hash_password, check_password
from flask import Flask, request, redirect
from extensions import db, login_manager, csrf, hash_password, check_password, limiter
from sqlalchemy import text
import markupsafe
from dotenv import load_dotenv
load_dotenv()
def nl2br(value):
@@ -30,10 +33,11 @@ def create_app():
Initializes Flask with:
- Secret key for session security
- SQLite database configuration
- Database configuration
- CSRF protection
- Login manager
- All route blueprints
- Security headers and HTTPS redirects
Handles database initialization and seeding with sample data if empty.
@@ -41,14 +45,23 @@ def create_app():
Flask: Configured Flask application instance.
"""
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'team-tryouts-secret-key-change-in-production')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///team_tryouts.db'
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
if not app.config['SECRET_KEY']:
raise RuntimeError('SECRET_KEY environment variable must be set for security')
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///team_tryouts.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['WTF_CSRF_ENABLED'] = True
# Secure session cookie settings
app.config['SESSION_COOKIE_SECURE'] = os.getenv('SESSION_COOKIE_SECURE', 'true').lower() == 'true'
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour session timeout
db.init_app(app)
login_manager.init_app(app)
csrf.init_app(app)
limiter.init_app(app)
from routes.auth import auth_bp
from routes.tryouts import tryouts_bp
@@ -69,6 +82,24 @@ def create_app():
# Register custom Jinja filters
app.jinja_env.filters['nl2br'] = nl2br
# Add security headers to all responses
@app.after_request
def add_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
# Force HTTPS in production (when not in debug mode)
@app.before_request
def force_https():
if not app.debug:
if not request.is_secure and request.headers.get('X-Forwarded-Proto') != 'https':
if os.getenv('FORCE_HTTPS', 'true').lower() == 'true':
return redirect(request.url.replace('http://', 'https://'), code=301)
with app.app_context():
import models
from models import User, MatchParticipant
@@ -101,4 +132,6 @@ def create_app():
if __name__ == '__main__':
app = create_app()
app.run(debug=True, host='0.0.0.0', port=5000)
# Debug mode should only be enabled via environment variable for security
debug_mode = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
app.run(debug=debug_mode, host='0.0.0.0', port=5000)
+1
View File
@@ -21,6 +21,7 @@ from dotenv import load_dotenv
load_dotenv()
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
print(DISCORD_BOT_TOKEN or 'FAILED TO PRINT BOT TOKEN')
# Configure logging
logger = logging.getLogger(__name__)
+8 -1
View File
@@ -2,7 +2,8 @@ from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from werkzeug.security import generate_password_hash, check_password_hash
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
# Database and extension initialization
db = SQLAlchemy()
@@ -11,6 +12,12 @@ login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'info'
csrf = CSRFProtect()
# Rate limiter for brute-force protection
limiter = Limiter(
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
def hash_password(password):
"""
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+24 -1
View File
@@ -5,13 +5,32 @@ This module handles user authentication including login, logout, and new user re
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from extensions import db, hash_password, check_password
from extensions import db, hash_password, check_password, limiter
from models import User, ESPORT_GAMES
from urllib.parse import urlparse
def is_safe_url(url):
"""Validate that a URL is safe for redirection (same origin).
Args:
url: The URL to validate.
Returns:
bool: True if the URL is safe (relative or same origin).
"""
if not url:
return False
parsed = url_parse(url)
# Allow relative URLs (no netloc) or same-origin URLs
return not parsed.netloc or parsed.netloc == request.host
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/login', methods=['GET', 'POST'])
@limiter.limit("10 per minute")
def login():
"""Handle user login authentication.
@@ -36,8 +55,12 @@ def login():
if not user.is_active_account:
flash('This account has been deactivated.', 'danger')
return render_template('pages/login.html')
# Regenerate session to prevent session fixation attacks
login_user(user)
# Validate redirect URL to prevent open redirect vulnerability
next_page = request.args.get('next')
if next_page and not is_safe_url(next_page):
next_page = None
flash(f'Welcome back, {user.full_name}!', 'success')
return redirect(next_page) if next_page else redirect(url_for('main.dashboard'))
else:
+56 -38
View File
@@ -12,6 +12,26 @@ from sqlalchemy import func
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
def validate_score(score_value):
"""Validate that a score is between 1 and 10.
Args:
score_value: The score value to validate (can be None, string, or int).
Returns:
int or None: The validated score or None if invalid/empty.
"""
if score_value is None:
return None
try:
score = int(score_value)
if 1 <= score <= 10:
return score
return None
except (ValueError, TypeError):
return None
@evaluations_bp.route('')
@login_required
def list_evaluations():
@@ -75,6 +95,14 @@ def evaluate_player(tryout_id, player_id):
flash('You do not have permission to evaluate players in this tryout.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
# Check if player is registered for this tryout
is_registered = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=player_id
).first() is not None
if not is_registered:
flash('Player is not registered for this tryout.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
player = User.query.get_or_404(player_id)
if player.role != 'player':
@@ -88,41 +116,31 @@ def evaluate_player(tryout_id, player_id):
).first()
if request.method == 'POST':
mecanics = request.form.get('mecanics_score')
cohesion = request.form.get('cohesion_score')
communication = request.form.get('communication_score')
gamesense = request.form.get('gamesense_score')
versatility = request.form.get('versatility_score')
discipline = request.form.get('discipline_score')
analysis = request.form.get('analysis_score')
sport_ethics = request.form.get('sport_ethics_score')
mental = request.form.get('mental_score')
mecanics = validate_score(request.form.get('mecanics_score'))
cohesion = validate_score(request.form.get('cohesion_score'))
communication = validate_score(request.form.get('communication_score'))
gamesense = validate_score(request.form.get('gamesense_score'))
versatility = validate_score(request.form.get('versatility_score'))
discipline = validate_score(request.form.get('discipline_score'))
analysis = validate_score(request.form.get('analysis_score'))
sport_ethics = validate_score(request.form.get('sport_ethics_score'))
mental = validate_score(request.form.get('mental_score'))
comments = request.form.get('comments')
position = request.form.get('position_recommendation')
scores = []
if mecanics: scores.append(int(mecanics))
if cohesion: scores.append(int(cohesion))
if communication: scores.append(int(communication))
if gamesense: scores.append(int(gamesense))
if versatility: scores.append(int(versatility))
if discipline: scores.append(int(discipline))
if analysis: scores.append(int(analysis))
if sport_ethics: scores.append(int(sport_ethics))
if mental: scores.append(int(mental))
scores = [s for s in [mecanics, cohesion, communication, gamesense, versatility, discipline, analysis, sport_ethics, mental] if s is not None]
overall = sum(scores) / len(scores) if scores else None
if existing_eval:
existing_eval.mecanics_score = int(mecanics) if mecanics else None
existing_eval.cohesion_score = int(cohesion) if cohesion else None
existing_eval.communication_score = int(communication) if communication else None
existing_eval.gamesense_score = int(gamesense) if gamesense else None
existing_eval.versatility_score = int(versatility) if versatility else None
existing_eval.discipline_score = int(discipline) if discipline else None
existing_eval.analysis_score = int(analysis) if analysis else None
existing_eval.sport_ethics_score = int(sport_ethics) if sport_ethics else None
existing_eval.mental_score = int(mental) if mental else None
existing_eval.mecanics_score = mecanics
existing_eval.cohesion_score = cohesion
existing_eval.communication_score = communication
existing_eval.gamesense_score = gamesense
existing_eval.versatility_score = versatility
existing_eval.discipline_score = discipline
existing_eval.analysis_score = analysis
existing_eval.sport_ethics_score = sport_ethics
existing_eval.mental_score = mental
existing_eval.overall_score = overall
existing_eval.comments = comments
existing_eval.position_recommendation = position
@@ -132,15 +150,15 @@ def evaluate_player(tryout_id, player_id):
tryout_id=tryout_id,
player_id=player_id,
evaluator_id=current_user.id,
mecanics_score=int(mecanics) if mecanics else None,
cohesion_score=int(cohesion) if cohesion else None,
communication_score=int(communication) if communication else None,
gamesense_score=int(gamesense) if gamesense else None,
versatility_score=int(versatility) if versatility else None,
discipline_score=int(discipline) if discipline else None,
analysis_score=int(analysis) if analysis else None,
sport_ethics_score=int(sport_ethics) if sport_ethics else None,
mental_score=int(mental) if mental else None,
mecanics_score=mecanics,
cohesion_score=cohesion,
communication_score=communication,
gamesense_score=gamesense,
versatility_score=versatility,
discipline_score=discipline,
analysis_score=analysis,
sport_ethics_score=sport_ethics,
mental_score=mental,
overall_score=overall,
comments=comments,
position_recommendation=position
+4 -1
View File
@@ -450,7 +450,10 @@ def edit_match(match_id):
return redirect(url_for('matches.calendar'))
teams = Team.query.filter_by(tryout_id=tryout.id).all()
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
# Only show players registered for this tryout
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout.id).all()
all_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
all_players = sorted([p for p in all_players if p], key=lambda x: x.full_name)
current_player_ids = [p.player_id for p in match.participants.all()]
# Get players grouped by team side for player_vs_player matches
team1_player_ids = [p.player_id for p in match.participants.filter_by(team_side=1).all()]
+5
View File
@@ -283,6 +283,11 @@ def add_player_note(team_id, player_id):
flash('Can only add notes for players.', 'danger')
return redirect(url_for('teams.list_teams'))
# Verify player belongs to this team
if player.team_id != team_id:
flash(f'{player.full_name} is not on {team.name}.', 'danger')
return redirect(url_for('teams.list_teams'))
content = request.form.get('content', '').strip()
if content:
+4 -2
View File
@@ -248,8 +248,10 @@ def view_tryout(tryout_id):
can_view_calendar = is_registered or player_in_match
# Get all players (for manager registration dropdown)
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
# Only expose all_players to users who can manage players in this tryout
all_players = None
if can_edit:
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
# Get matches for this tryout with participant info
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date).all()
+1
View File
@@ -20,6 +20,7 @@ users_bp = Blueprint('users', __name__, url_prefix='/users')
def update_user_gamertags(user, selected_games):
"""Update gamertags for a user based on form input.
++++++++++++++++++++++++++++++
Handles creating, updating, and deleting gamertag records for the specified games.
Used by both edit_user and edit_profile routes to avoid code duplication.