422.45 Security hardening and fault tolerance
Strengthen systems to resist attacks and continue operating under failure conditions.
422.45 Security Hardening and Fault Tolerance
Strengthen systems to resist attacks and continue operating under failure conditions.
Overview
Security hardening and fault tolerance are advanced strategies that improve the resilience of software systems. Hardening involves reducing the system's attack surface by disabling unnecessary features, tightening configurations, and enforcing strict policies. Fault tolerance ensures that the system continues functioning, even when components fail or unexpected errors occur.
Together, these techniques help create robust Flask applications that perform safely and predictably, even under pressure from attackers or unexpected failures.
Learning Targets
In this topic, students learn to:
Apply techniques for hardening systems to reduce vulnerabilities
Understand how fault-tolerant systems recover from failure
Evaluate the effectiveness of configuration settings and error handling
Design software with built-in resilience
What is Security Hardening?
Security hardening refers to systematically reducing a system's vulnerabilities by removing unnecessary features, applying secure configurations, and following security best practices.
Flask Application Hardening
from flask import Flask
import secrets
app = Flask(__name__)
# HARDENING: Secure configuration
app.config.update(
# Strong secret key for sessions
SECRET_KEY=secrets.token_hex(32),
# Security headers
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_HTTPONLY=True, # No JavaScript access
SESSION_COOKIE_SAMESITE='Strict', # CSRF protection
# Disable debug mode in production
DEBUG=False,
# Limit request size (prevent DoS)
MAX_CONTENT_LENGTH=16 * 1024 * 1024 # 16MB limit
)
@app.after_request
def add_security_headers(response):
"""Add security headers to all responses"""
# Prevent clickjacking
response.headers['X-Frame-Options'] = 'DENY'
# Prevent MIME type sniffing
response.headers['X-Content-Type-Options'] = 'nosniff'
# XSS protection
response.headers['X-XSS-Protection'] = '1; mode=block'
# HTTPS enforcement (if using HTTPS)
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
# Remove server information (security through obscurity)
@app.after_request
def remove_server_header(response):
response.headers.pop('Server', None)
return response
Common Hardening Practices
# 1. Remove debug information in production
if app.config['DEBUG']:
print("WARNING: Debug mode enabled - disable in production!")
# 2. Validate all inputs
def validate_user_input(data):
"""Validate and sanitise user input"""
if not data or len(data) > 1000:
return None
# Remove dangerous characters
dangerous_chars = ['<', '>', '"', "'", '&', ';']
for char in dangerous_chars:
data = data.replace(char, '')
return data.strip()
# 3. Use environment variables for sensitive config
import os
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///default.db')
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-key-change-in-production')
# 4. Implement rate limiting
from collections import defaultdict
from datetime import datetime, timedelta
request_counts = defaultdict(list)
def rate_limit_check(ip_address, max_requests=100, time_window=3600):
"""Simple rate limiting: max 100 requests per hour"""
now = datetime.now()
cutoff = now - timedelta(seconds=time_window)
# Clean old requests
request_counts[ip_address] = [
req_time for req_time in request_counts[ip_address]
if req_time > cutoff
]
# Check if over limit
if len(request_counts[ip_address]) >= max_requests:
return False
# Record this request
request_counts[ip_address].append(now)
return True
@app.before_request
def check_rate_limit():
client_ip = request.environ.get('REMOTE_ADDR', '0.0.0.0')
if not rate_limit_check(client_ip):
return "Too many requests", 429
What is Fault Tolerance?
Fault tolerance is the ability of a system to continue operating when part of it fails. This is essential for reliable applications where errors shouldn't crash the entire system.
Database Connection Fault Tolerance
import sqlite3
import time
def get_db_connection(max_retries=3, retry_delay=1):
"""
Get database connection with retry logic
"""
for attempt in range(max_retries):
try:
conn = sqlite3.connect('app.db', timeout=10)
return conn
except sqlite3.Error as e:
print(f"Database connection attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
time.sleep(retry_delay)
else:
raise
@app.route('/users')
def get_users():
try:
# FAULT TOLERANT: Retry database connection
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
users = cursor.fetchall()
conn.close()
return render_template('users.html', users=users)
except sqlite3.Error:
# GRACEFUL DEGRADATION: Show error page instead of crashing
return render_template('error.html',
message="Database temporarily unavailable"), 503
Error Handling and Graceful Degradation
@app.errorhandler(404)
def page_not_found(error):
"""Handle 404 errors gracefully"""
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
"""Handle server errors gracefully"""
# Log the error for debugging
app.logger.error(f"Server Error: {error}")
# Return user-friendly error page
return render_template('error.html',
message="Something went wrong. Please try again later."), 500
@app.route('/search')
def search():
try:
query = request.args.get('q', '')
# Validate input
if not query:
return render_template('search.html', results=[], error="Please enter a search term")
# Perform search
results = search_database(query)
return render_template('search.html', results=results)
except Exception as e:
# FAIL SAFELY: Return empty results instead of crashing
app.logger.error(f"Search error: {e}")
return render_template('search.html', results=[],
error="Search temporarily unavailable")
def search_database(query):
"""Search with error handling"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM posts WHERE content LIKE ?",
(f'%{query}%',))
results = cursor.fetchall()
conn.close()
return results
except sqlite3.Error:
# Return empty list if database fails
return []
Input Validation and Sanitisation
@app.route('/profile', methods=['POST'])
def update_profile():
try:
# FAULT TOLERANT: Validate all inputs
name = request.form.get('name', '').strip()
email = request.form.get('email', '').strip()
bio = request.form.get('bio', '').strip()
# Validation with specific error messages
errors = []
if not name or len(name) < 2:
errors.append("Name must be at least 2 characters")
if not email or '@' not in email:
errors.append("Please enter a valid email address")
if len(bio) > 500:
errors.append("Bio must be less than 500 characters")
if errors:
return render_template('profile.html', errors=errors)
# Update database
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"UPDATE users SET name=?, email=?, bio=? WHERE id=?",
(name, email, bio, session['user_id'])
)
conn.commit()
conn.close()
return render_template('profile.html', success="Profile updated successfully")
except Exception as e:
# FAIL SAFELY: Log error and show user-friendly message
app.logger.error(f"Profile update error: {e}")
return render_template('profile.html',
errors=["Unable to update profile. Please try again."])
Monitoring and Logging
import logging
from datetime import datetime
# Configure logging
logging.basicConfig(
filename='app.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
@app.before_request
def log_request():
"""Log all requests for monitoring"""
app.logger.info(f"Request: {request.method} {request.path} from {request.remote_addr}")
def log_security_event(event_type, details):
"""Log security-related events"""
app.logger.warning(f"SECURITY: {event_type} - {details}")
@app.route('/login', methods=['POST'])
def login_with_monitoring():
username = request.form['username']
client_ip = request.remote_addr
if authenticate_user(username, request.form['password']):
app.logger.info(f"Successful login: {username} from {client_ip}")
session['user_id'] = get_user_id(username)
return redirect('/dashboard')
else:
# Log failed login attempts
log_security_event("Failed Login", f"User: {username}, IP: {client_ip}")
return "Invalid credentials"
Configuration Management
# Different configurations for different environments
class Config:
"""Base configuration"""
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///app.db')
class DevelopmentConfig(Config):
"""Development configuration"""
DEBUG = True
SESSION_COOKIE_SECURE = False # Allow HTTP in development
class ProductionConfig(Config):
"""Production configuration - more secure"""
DEBUG = False
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_HTTPONLY = True
PERMANENT_SESSION_LIFETIME = 1800 # 30 minutes
# Load appropriate config
config_name = os.getenv('FLASK_ENV', 'development')
if config_name == 'production':
app.config.from_object(ProductionConfig)
else:
app.config.from_object(DevelopmentConfig)
Code Interpretation Examples
# Example 1: Identify the security issue
@app.route('/admin')
def admin_panel():
return render_template('admin.html')
# Problems:
# 1. No authentication check
# 2. No access control
# 3. Admin panel accessible to anyone
# Example 2: Hardened version
@app.route('/admin')
def admin_panel():
# Check authentication
if 'user_id' not in session:
return redirect('/login')
# Check admin privileges
if not is_admin_user(session['user_id']):
log_security_event("Unauthorised Admin Access",
f"User {session['user_id']} attempted admin access")
return "Access denied", 403
return render_template('admin.html')
# Example 3: Fault tolerance in file upload
@app.route('/upload', methods=['POST'])
def upload_file():
try:
if 'file' not in request.files:
return "No file provided", 400
file = request.files['file']
# Validate file
if file.filename == '':
return "No file selected", 400
# Check file size (prevent DoS)
if len(file.read()) > 5 * 1024 * 1024: # 5MB limit
return "File too large", 400
file.seek(0) # Reset file pointer
# Save file safely
filename = secure_filename(file.filename)
file.save(f'uploads/{filename}')
return "File uploaded successfully"
except Exception as e:
app.logger.error(f"File upload error: {e}")
return "Upload failed. Please try again.", 500
Summary
Security hardening reduces the attack surface through secure configuration and the removal of unnecessary features
Fault tolerance ensures systems continue working even when components fail
Error handling should be graceful - show user-friendly messages, not system errors
Monitoring and logging help detect issues and security events
Input validation prevents many types of attacks and system failures
Configuration management allows different security settings for development and production
Building resilient Flask applications requires thinking about both security and reliability from the start. By implementing proper error handling, secure configuration, and monitoring, you create applications that can withstand both attacks and unexpected failures.
Last updated
Was this helpful?