422.46 File and hardware protection

Protect user data and physical devices by controlling how files are accessed, stored, and handled during execution.

422.46 File and Hardware Protection

Protect user data and physical devices by controlling how files are accessed, stored, and handled during execution.

Overview

Software that handles files or interacts with physical devices must be written with care. Unvalidated file input, insecure file storage, or unauthorised hardware access can lead to serious security breaches. Additionally, side-channel attacks can reveal sensitive information by observing how software interacts with the underlying system.

This topic focuses on how developers can reduce these risks through secure file handling and defensive coding patterns that limit exposure to hardware-based vulnerabilities in Flask applications.

Learning Targets

In this topic, students learn to:

  • Identify common file and hardware vulnerabilities

  • Apply safe file handling and access controls in Flask applications

  • Design programs that minimise exposure to physical attack vectors

  • Understand the risks of side-channel attacks and how to prevent them

File Security Vulnerabilities

File handling in web applications creates multiple security risks that attackers can exploit to access unauthorised data or execute malicious code.

Dangerous File Upload Example

from flask import Flask, request
import os

app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload_file():
    # VULNERABLE: No validation of uploaded files
    file = request.files['file']
    filename = file.filename
    
    # DANGEROUS: Direct use of user-provided filename
    file.save(f'uploads/{filename}')
    
    return f"File {filename} uploaded successfully"

# Attack examples:
# - filename = "../../app.py" (overwrite application code)
# - filename = "malicious.php" (upload executable code)
# - filename = "virus.exe" (upload malware)

Secure File Upload Implementation

import os
from werkzeug.utils import secure_filename

# Define allowed file types
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB

def allowed_file(filename):
    """Check if file type is allowed"""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def get_file_size(file):
    """Get file size safely"""
    file.seek(0, os.SEEK_END)
    size = file.tell()
    file.seek(0)  # Reset file pointer
    return size

@app.route('/upload_secure', methods=['POST'])
def upload_file_secure():
    try:
        # Check if file was uploaded
        if 'file' not in request.files:
            return "No file provided", 400
        
        file = request.files['file']
        
        # Check if filename is provided
        if file.filename == '':
            return "No file selected", 400
        
        # Validate file type
        if not allowed_file(file.filename):
            return "File type not allowed. Only txt, pdf, png, jpg, jpeg, gif files are permitted", 400
        
        # Check file size
        file_size = get_file_size(file)
        if file_size > MAX_FILE_SIZE:
            return f"File too large. Maximum size is {MAX_FILE_SIZE // (1024*1024)}MB", 400
        
        # Secure the filename
        filename = secure_filename(file.filename)
        
        # Add timestamp to prevent filename conflicts
        import time
        timestamp = str(int(time.time()))
        name, ext = os.path.splitext(filename)
        safe_filename = f"{name}_{timestamp}{ext}"
        
        # Ensure upload directory exists
        upload_dir = 'uploads'
        os.makedirs(upload_dir, exist_ok=True)
        
        # Save file safely
        file_path = os.path.join(upload_dir, safe_filename)
        file.save(file_path)
        
        # Set restrictive file permissions (Unix/Linux)
        os.chmod(file_path, 0o644)  # Read/write for owner, read for others
        
        return f"File uploaded successfully as {safe_filename}"
        
    except Exception as e:
        app.logger.error(f"File upload error: {e}")
        return "Upload failed. Please try again.", 500

# Benefits of this approach:
# 1. Only allowed file types accepted
# 2. Filenames sanitised to prevent path traversal
# 3. File size limits prevent DoS attacks
# 4. Timestamps prevent filename conflicts
# 5. Restrictive file permissions

Path Traversal Prevention

@app.route('/download/<filename>')
def download_file_vulnerable(filename):
    # VULNERABLE: Direct file access
    return send_file(f'uploads/{filename}')

# Attack: /download/../../../etc/passwd
# Could access system files outside upload directory

@app.route('/download_secure/<filename>')
def download_file_secure(filename):
    # SECURE: Validate and restrict file access
    
    # Basic filename validation
    if not filename or '..' in filename or filename.startswith('/'):
        return "Invalid filename", 400
    
    # Ensure filename is safe
    safe_filename = secure_filename(filename)
    if safe_filename != filename:
        return "Invalid filename", 400
    
    # Construct full path
    upload_dir = os.path.abspath('uploads')
    file_path = os.path.join(upload_dir, safe_filename)
    
    # Ensure file is within upload directory
    if not file_path.startswith(upload_dir):
        return "Access denied", 403
    
    # Check if file exists
    if not os.path.isfile(file_path):
        return "File not found", 404
    
    return send_file(file_path, as_attachment=True)

File Content Validation

def validate_image_file(file_path):
    """
    Validate that uploaded image file is actually an image
    """
    try:
        from PIL import Image
        
        # Try to open as image
        with Image.open(file_path) as img:
            # Verify it's a valid image format
            img.verify()
            return True
    except Exception:
        return False

def scan_file_content(file_path):
    """
    Basic content scanning for dangerous patterns
    """
    dangerous_patterns = [
        b'<script',       # JavaScript
        b'<?php',         # PHP code
        b'exec(',         # Code execution
        b'system(',       # System commands
        b'shell_exec(',   # Shell execution
    ]
    
    try:
        with open(file_path, 'rb') as f:
            content = f.read(1024)  # Read first 1KB
            
            for pattern in dangerous_patterns:
                if pattern in content.lower():
                    return False
        return True
    except Exception:
        return False

@app.route('/upload_validated', methods=['POST'])
def upload_with_validation():
    file = request.files['file']
    
    # Basic validation (as before)
    if not allowed_file(file.filename):
        return "Invalid file type", 400
    
    # Save temporarily for content validation
    temp_path = f'temp_{secure_filename(file.filename)}'
    file.save(temp_path)
    
    try:
        # Validate file content based on type
        if file.filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')):
            if not validate_image_file(temp_path):
                return "Invalid image file", 400
        
        # Scan for dangerous content
        if not scan_file_content(temp_path):
            return "File contains potentially dangerous content", 400
        
        # Move to final location if validation passes
        final_path = f'uploads/{secure_filename(file.filename)}'
        os.rename(temp_path, final_path)
        
        return "File uploaded and validated successfully"
        
    finally:
        # Clean up temporary file
        if os.path.exists(temp_path):
            os.remove(temp_path)

Side-Channel Attack Prevention

Side-channel attacks exploit indirect information leaked by a program's execution, such as timing differences or resource usage patterns.

Timing Attack Prevention

import time
import hmac

def vulnerable_password_check(stored_password, provided_password):
    """
    VULNERABLE: Timing attack possible
    """
    # This comparison stops at first different character
    # Attacker can measure response time to guess password character by character
    if stored_password == provided_password:
        return True
    return False

def secure_password_check(stored_password, provided_password):
    """
    SECURE: Constant-time comparison
    """
    # hmac.compare_digest performs constant-time comparison
    return hmac.compare_digest(stored_password, provided_password)

@app.route('/login', methods=['POST'])
def login_timing_safe():
    username = request.form['username']
    password = request.form['password']
    
    # Get stored password hash
    stored_hash = get_user_password_hash(username)
    
    if stored_hash:
        # Use timing-safe comparison
        if secure_password_check(stored_hash, hash_password(password)):
            session['user_id'] = get_user_id(username)
            return "Login successful"
    
    # Add consistent delay regardless of success/failure
    time.sleep(0.1)  # Prevents timing analysis of user enumeration
    return "Invalid credentials"

Information Leakage Prevention

@app.route('/user/<username>')
def get_user_profile(username):
    """
    Example of preventing information leakage
    """
    try:
        user = get_user_by_username(username)
        
        if not user:
            # Don't reveal whether username exists
            return "User not found", 404
        
        # Check if current user can view this profile
        if not can_view_profile(session.get('user_id'), user['id']):
            # Same response as non-existent user
            return "User not found", 404
        
        return render_template('profile.html', user=user)
        
    except Exception as e:
        # Don't leak error details
        app.logger.error(f"Profile access error: {e}")
        return "User not found", 404

def can_view_profile(viewer_id, profile_user_id):
    """Check if user can view another user's profile"""
    if not viewer_id:
        return False
    
    # Users can always view their own profile
    if viewer_id == profile_user_id:
        return True
    
    # Check if profile is public
    return is_profile_public(profile_user_id)

Secure File Storage Practices

import json
import os
from cryptography.fernet import Fernet

class SecureFileStorage:
    """
    Secure file storage with encryption
    """
    
    def __init__(self, storage_dir='secure_storage'):
        self.storage_dir = storage_dir
        os.makedirs(storage_dir, exist_ok=True)
        
        # Generate or load encryption key
        self.key = self.get_or_create_key()
        self.cipher = Fernet(self.key)
    
    def get_or_create_key(self):
        """Get existing key or create new one"""
        key_file = 'file_encryption.key'
        
        if os.path.exists(key_file):
            with open(key_file, 'rb') as f:
                return f.read()
        else:
            # Generate new key
            key = Fernet.generate_key()
            with open(key_file, 'wb') as f:
                f.write(key)
            # Set restrictive permissions on key file
            os.chmod(key_file, 0o600)  # Owner read/write only
            return key
    
    def store_secure_data(self, data, filename):
        """Store data with encryption"""
        try:
            # Convert to bytes if necessary
            if isinstance(data, str):
                data = data.encode('utf-8')
            elif isinstance(data, dict):
                data = json.dumps(data).encode('utf-8')
            
            # Encrypt data
            encrypted_data = self.cipher.encrypt(data)
            
            # Store in secure directory
            file_path = os.path.join(self.storage_dir, secure_filename(filename))
            with open(file_path, 'wb') as f:
                f.write(encrypted_data)
            
            # Set restrictive permissions
            os.chmod(file_path, 0o600)
            
            return True
        except Exception as e:
            app.logger.error(f"Secure storage error: {e}")
            return False
    
    def retrieve_secure_data(self, filename):
        """Retrieve and decrypt data"""
        try:
            file_path = os.path.join(self.storage_dir, secure_filename(filename))
            
            if not os.path.exists(file_path):
                return None
            
            with open(file_path, 'rb') as f:
                encrypted_data = f.read()
            
            # Decrypt data
            decrypted_data = self.cipher.decrypt(encrypted_data)
            return decrypted_data.decode('utf-8')
            
        except Exception as e:
            app.logger.error(f"Secure retrieval error: {e}")
            return None

# Usage example
secure_storage = SecureFileStorage()

@app.route('/store_sensitive', methods=['POST'])
def store_sensitive_data():
    if 'user_id' not in session:
        return "Unauthorised", 401
    
    sensitive_data = request.form['data']
    user_id = session['user_id']
    
    # Store encrypted data
    filename = f"user_{user_id}_data.enc"
    if secure_storage.store_secure_data(sensitive_data, filename):
        return "Data stored securely"
    else:
        return "Storage failed", 500

Hardware and System Protection

import psutil
import os

def check_system_resources():
    """
    Monitor system resources to prevent abuse
    """
    # Check memory usage
    memory = psutil.virtual_memory()
    if memory.percent > 90:
        return False, "High memory usage"
    
    # Check disk space
    disk = psutil.disk_usage('/')
    if disk.percent > 95:
        return False, "Low disk space"
    
    # Check CPU usage
    cpu_percent = psutil.cpu_percent(interval=1)
    if cpu_percent > 95:
        return False, "High CPU usage"
    
    return True, "Resources OK"

@app.before_request
def resource_check():
    """Check system resources before processing requests"""
    if request.endpoint in ['upload', 'process_data']:
        is_ok, message = check_system_resources()
        if not is_ok:
            app.logger.warning(f"Resource check failed: {message}")
            return "Service temporarily unavailable", 503

def secure_temp_file_handling():
    """
    Secure temporary file creation and cleanup
    """
    import tempfile
    
    # Create temporary file in secure location
    temp_fd, temp_path = tempfile.mkstemp(
        suffix='.tmp',
        prefix='secure_',
        dir='/tmp'  # Or app-specific temp directory
    )
    
    try:
        # Set restrictive permissions
        os.chmod(temp_path, 0o600)
        
        # Use the temporary file
        with os.fdopen(temp_fd, 'w') as temp_file:
            temp_file.write("Sensitive temporary data")
        
        # Process the file...
        
    finally:
        # Always clean up temporary files
        try:
            os.unlink(temp_path)
        except OSError:
            pass  # File already deleted

Code Interpretation Examples

# Example 1: Identify file security issue
@app.route('/view_file')
def view_file():
    filename = request.args.get('file')
    with open(filename, 'r') as f:
        return f.read()

# Problems:
# 1. No path validation - can access any file
# 2. No access control
# 3. Could expose system files
# Example 2: Timing attack vulnerability
def check_api_key(provided_key):
    correct_key = "secret123"
    for i in range(len(correct_key)):
        if provided_key[i] != correct_key[i]:
            return False
    return True

# Problem: Returns False as soon as wrong character found
# Timing difference reveals information about correct key
# Example 3: Secure file validation
@app.route('/upload', methods=['POST'])
def secure_upload():
    file = request.files.get('file')
    
    # Comprehensive validation
    if not file or file.filename == '':
        return "No file provided", 400
    
    # Check file type
    if not file.filename.endswith(('.txt', '.pdf')):
        return "Invalid file type", 400
    
    # Check file size
    if len(file.read()) > 1024 * 1024:  # 1MB
        return "File too large", 400
    
    file.seek(0)  # Reset file pointer
    
    # Secure filename and save
    safe_name = secure_filename(file.filename)
    file.save(f'uploads/{safe_name}')
    
    return "File uploaded successfully"

Summary

  • File upload security requires validation of type, size, content, and filename sanitisation

  • Path traversal prevention ensures files are accessed only from intended directories

  • Side-channel attacks can be prevented through constant-time operations and by avoiding information leakage

  • Secure file storage may require encryption for sensitive data

  • System resource monitoring prevents abuse and denial-of-service attacks

  • Temporary file handling must include proper cleanup and secure permissions

File and hardware protection requires a comprehensive approach that considers both the immediate security of file operations and the broader system implications. By implementing proper validation, access controls, and monitoring, Flask applications can safely handle file operations while protecting against various attack vectors.

Last updated

Was this helpful?