feat: add input validation, security headers, and error handling to Flask app

This commit is contained in:
Greg 2025-05-17 12:26:29 +02:00
parent 7dce92f5c1
commit 118eb82393

57
app.py
View File

@ -3,9 +3,24 @@ from flask import Flask, render_template, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func
import os
import logging
from flask_wtf import CSRFProtect
from flask_talisman import Talisman
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'change-this-secret-key')
# CSRF Protection
csrf = CSRFProtect(app)
# Security Headers
Talisman(app)
# Logging Configuration
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
db = SQLAlchemy(app)
# Models
@ -74,8 +89,38 @@ def db_to_json():
"guestNames": guest_names
}
import re
def validate_player_name(name):
# Only allow a-z, A-Z, 0-9, spaces, hyphens, and periods, max 50 chars
return bool(re.fullmatch(r'[a-zA-Z0-9 .-]{1,50}', name))
def validate_guest_name(name):
# Only allow a-z, A-Z, 0-9, spaces, hyphens, and periods, max 50 chars
return bool(re.fullmatch(r'[a-zA-Z0-9 .-]{1,50}', name))
def validate_date_str(date_str):
# Format: DD/MM/YY
return bool(re.fullmatch(r'\d{2}/\d{2}/\d{2}', date_str))
def json_to_db(data):
try:
# Validate players
for name in data.get("players", []):
if not validate_player_name(name):
logger.warning(f"Invalid player name: {name}")
raise ValueError("Invalid player name.")
# Validate dates
for date_str in data.get("dates", []):
if not validate_date_str(date_str):
logger.warning(f"Invalid date string: {date_str}")
raise ValueError("Invalid date string.")
# Validate guest names
for guest_name in data.get("guestNames", {}).values():
if not validate_guest_name(guest_name):
logger.warning(f"Invalid guest name: {guest_name}")
raise ValueError("Invalid guest name.")
# Start a single transaction for all operations
with db.session.begin():
# Clear existing attendance records
@ -120,9 +165,8 @@ def json_to_db(data):
# The transaction will be committed automatically if no exceptions occur
except Exception as e:
# The transaction will be rolled back automatically if an exception occurs
print("Error during database operation:", e)
raise
logger.error(f"Error during database operation: {e}")
raise ValueError("An error occurred while processing the data.")
@ -140,10 +184,17 @@ def get_data():
return jsonify(db_to_json())
@app.route('/api/data', methods=['POST'])
@csrf.exempt # Remove this line if you want to enforce CSRF on API endpoints (for APIs, you may want to handle CSRF differently)
def update_data():
data = request.json
try:
json_to_db(data)
return jsonify({"status": "success"})
except ValueError as e:
return jsonify({"status": "error", "message": str(e)}), 400
except Exception as e:
logger.error(f"Unexpected error in /api/data POST: {e}")
return jsonify({"status": "error", "message": "Internal server error."}), 500
# Optional: export from DB as JSON (for compatibility)
@app.route('/export-data')