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?