Chapter 8: Exception Handling and Security Checks

Haiyue
25min

Chapter 8: Exception Handling and Security Checks

Learning Objectives
  • Master Pylint’s exception handling checking features
  • Understand security-related code checking rules
  • Learn to write robust exception handling code
  • Master best practices for secure programming

Key Concepts

Exception Handling Check Types

🔄 正在渲染 Mermaid 图表...

Exception Handling Best Practices

PrincipleDescriptionPylint Check
Specific CatchingCatch specific exception typesbroad-except
Exception ChainPreserve original exception informationraise-missing-from
Resource CleanupEnsure resources are properly released-
LoggingLog detailed exception information-

Code Examples

Exception Handling Issues and Optimization

# W0702: bare-except - Bare except statement
def bad_exception_handling():
    """Bad exception handling example"""
    try:
        result = 10 / 0
        return result
    except:  # pylint: disable=bare-except
        # Catches all exceptions, including SystemExit, KeyboardInterrupt, etc.
        print("Something went wrong")
        return None

# Fix solution 1: Catch specific exceptions
def good_exception_handling():
    """Good exception handling example"""
    try:
        result = 10 / 0
        return result
    except ZeroDivisionError as e:
        print(f"Division by zero error: {e}")
        return None
    except Exception as e:
        print(f"Unexpected error: {e}")
        return None

# W0703: broad-except - Overly broad exception catching
def bad_broad_exception():
    """Overly broad exception catching"""
    try:
        # Multiple possible exceptions
        with open('file.txt', 'r') as f:
            data = f.read()

        number = int(data)
        result = 100 / number
        return result
    except Exception as e:  # pylint: disable=broad-except
        # Catches all Exception, cannot distinguish different error types
        print(f"Error: {e}")
        return None

# Fix solution: Handle different exception types separately
def good_specific_exception():
    """Specific exception handling"""
    try:
        with open('file.txt', 'r', encoding='utf-8') as f:
            data = f.read().strip()

        number = int(data)
        result = 100 / number
        return result

    except FileNotFoundError:
        print("File not found: file.txt")
        return None

    except PermissionError:
        print("Permission denied to read file.txt")
        return None

    except ValueError as e:
        print(f"Invalid number format in file: {e}")
        return None

    except ZeroDivisionError:
        print("Cannot divide by zero")
        return None

    except Exception as e:
        # Only as a final safeguard
        print(f"Unexpected error: {e}")
        return None

# W0707: raise-missing-from - Missing from clause when re-raising exception
def bad_reraise():
    """Bad exception re-raising"""
    try:
        with open('config.json', 'r') as f:
            import json
            config = json.load(f)
        return config
    except (FileNotFoundError, json.JSONDecodeError):
        # Loses original exception information
        raise ValueError("Configuration file is invalid")  # pylint: disable=raise-missing-from

# Fix solution: Use raise...from to preserve exception chain
def good_reraise():
    """Good exception re-raising"""
    try:
        with open('config.json', 'r', encoding='utf-8') as f:
            import json
            config = json.load(f)
        return config
    except FileNotFoundError as e:
        raise ValueError("Configuration file not found") from e
    except json.JSONDecodeError as e:
        raise ValueError("Configuration file format is invalid") from e

# Custom exception class usage
class ConfigurationError(Exception):
    """Configuration error exception"""

    def __init__(self, message, config_file=None, line_number=None):
        super().__init__(message)
        self.config_file = config_file
        self.line_number = line_number

    def __str__(self):
        base_msg = super().__str__()
        if self.config_file:
            base_msg += f" in file '{self.config_file}'"
        if self.line_number:
            base_msg += f" at line {self.line_number}"
        return base_msg

def parse_config_file(filename):
    """Parse configuration file"""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            import json
            config = json.load(f)

        # Validate required configuration items
        required_keys = ['database_url', 'api_key', 'debug']
        missing_keys = [key for key in required_keys if key not in config]

        if missing_keys:
            raise ConfigurationError(
                f"Missing required configuration keys: {missing_keys}",
                config_file=filename
            )

        return config

    except FileNotFoundError as e:
        raise ConfigurationError(
            "Configuration file not found",
            config_file=filename
        ) from e

    except json.JSONDecodeError as e:
        raise ConfigurationError(
            "Invalid JSON format in configuration file",
            config_file=filename,
            line_number=e.lineno
        ) from e

File and Encoding Security

# W1514: unspecified-encoding - Unspecified encoding
def bad_file_handling():
    """Bad file handling - unspecified encoding"""
    # System default encoding may cause cross-platform issues
    with open('data.txt', 'r') as f:  # pylint: disable=unspecified-encoding
        content = f.read()
    return content

# Fix solution: Explicitly specify encoding
def good_file_handling():
    """Good file handling - explicitly specify encoding"""
    try:
        with open('data.txt', 'r', encoding='utf-8') as f:
            content = f.read()
        return content
    except UnicodeDecodeError as e:
        print(f"Encoding error: {e}")
        # Try other encodings
        try:
            with open('data.txt', 'r', encoding='gbk') as f:
                content = f.read()
            return content
        except UnicodeDecodeError:
            # Use error handling strategy
            with open('data.txt', 'r', encoding='utf-8', errors='replace') as f:
                content = f.read()
            return content

# Safe file handling class
class SafeFileHandler:
    """Safe file handler"""

    def __init__(self, default_encoding='utf-8'):
        self.default_encoding = default_encoding
        self.fallback_encodings = ['utf-8', 'gbk', 'latin1']

    def read_file(self, filename, encoding=None):
        """Safely read file"""
        encodings_to_try = [encoding] if encoding else self.fallback_encodings

        for enc in encodings_to_try:
            if enc is None:
                continue
            try:
                with open(filename, 'r', encoding=enc) as f:
                    return f.read(), enc
            except UnicodeDecodeError:
                continue
            except FileNotFoundError as e:
                raise FileNotFoundError(f"File not found: {filename}") from e

        # Finally try with replacement strategy
        try:
            with open(filename, 'r', encoding='utf-8', errors='replace') as f:
                content = f.read()
                return content, 'utf-8-replaced'
        except Exception as e:
            raise IOError(f"Unable to read file {filename}") from e

    def write_file(self, filename, content, encoding=None):
        """Safely write file"""
        enc = encoding or self.default_encoding
        try:
            with open(filename, 'w', encoding=enc) as f:
                f.write(content)
        except UnicodeEncodeError as e:
            # Use error handling strategy
            with open(filename, 'w', encoding=enc, errors='replace') as f:
                f.write(content)
            print(f"Warning: Some characters were replaced due to encoding issues: {e}")

Dangerous Function Usage Checks

# S102: exec-used - Using exec function
def bad_dynamic_execution():
    """Bad dynamic code execution"""
    user_input = "print('Hello, World!')"
    exec(user_input)  # pylint: disable=exec-used
    # Security risk: May execute malicious code

# Fix solution 1: Use safe alternatives
import ast

def safe_dynamic_execution():
    """Safe dynamic execution"""
    user_input = "1 + 2 * 3"

    try:
        # Parse to AST
        tree = ast.parse(user_input, mode='eval')

        # Check if it only contains safe nodes
        safe_nodes = (ast.Expression, ast.Constant, ast.Name, ast.Load,
                     ast.BinOp, ast.Add, ast.Sub, ast.Mult, ast.Div)

        for node in ast.walk(tree):
            if not isinstance(node, safe_nodes):
                raise ValueError(f"Unsafe operation: {type(node).__name__}")

        # Safe execution
        result = eval(compile(tree, '<string>', 'eval'))
        return result

    except (SyntaxError, ValueError) as e:
        print(f"Invalid or unsafe expression: {e}")
        return None

# Fix solution 2: Use configuration file instead of dynamic code
import json
from typing import Dict, Any

class SafeConfigProcessor:
    """Safe configuration processor"""

    def __init__(self):
        self.allowed_operations = {
            'add': lambda a, b: a + b,
            'multiply': lambda a, b: a * b,
            'format': lambda template, **kwargs: template.format(**kwargs)
        }

    def process_config(self, config: Dict[str, Any]) -> Any:
        """Process configuration file"""
        operation = config.get('operation')
        if operation not in self.allowed_operations:
            raise ValueError(f"Unsupported operation: {operation}")

        func = self.allowed_operations[operation]
        args = config.get('args', [])
        kwargs = config.get('kwargs', {})

        try:
            return func(*args, **kwargs)
        except Exception as e:
            raise ValueError(f"Error executing operation {operation}: {e}") from e

# S301: pickle-load - Using pickle.load
import pickle

def bad_pickle_usage():
    """Bad pickle usage"""
    with open('data.pkl', 'rb') as f:
        data = pickle.load(f)  # pylint: disable=pickle-load
    return data

# Fix solution: Use safe serialization formats
import json
from dataclasses import dataclass, asdict
from typing import Union, Dict, List

@dataclass
class SafeData:
    """Safe data class"""
    name: str
    value: Union[int, float, str]
    metadata: Dict[str, str]

class SafeSerializer:
    """Safe serializer"""

    @staticmethod
    def serialize_to_json(data: Union[Dict, List, SafeData], filename: str):
        """Serialize to JSON file"""
        if isinstance(data, SafeData):
            data = asdict(data)

        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

    @staticmethod
    def deserialize_from_json(filename: str) -> Union[Dict, List]:
        """Deserialize from JSON file"""
        with open(filename, 'r', encoding='utf-8') as f:
            data = json.load(f)
        return data

    @staticmethod
    def create_safe_data(data_dict: Dict) -> SafeData:
        """Create safe data object"""
        return SafeData(
            name=str(data_dict.get('name', '')),
            value=data_dict.get('value', 0),
            metadata=data_dict.get('metadata', {})
        )

Safe Process Execution

# B605: start-process-with-shell - Starting process with shell
import subprocess

def bad_subprocess_usage():
    """Bad subprocess usage"""
    user_input = "ls -la"
    # Security risk: shell injection
    result = subprocess.run(user_input, shell=True, capture_output=True, text=True)
    return result.stdout

# Fix solution: Use safe process execution
import shlex
from typing import List, Optional

class SafeProcessExecutor:
    """Safe process executor"""

    def __init__(self):
        self.allowed_commands = {
            'ls': ['ls'],
            'cat': ['cat'],
            'echo': ['echo'],
            'grep': ['grep'],
            'find': ['find']
        }

    def execute_command(self, command: str, args: List[str] = None) -> Optional[str]:
        """Safely execute command"""
        if command not in self.allowed_commands:
            raise ValueError(f"Command not allowed: {command}")

        cmd_list = self.allowed_commands[command].copy()

        if args:
            # Validate argument safety
            safe_args = []
            for arg in args:
                # Remove dangerous characters
                safe_arg = self._sanitize_argument(arg)
                safe_args.append(safe_arg)
            cmd_list.extend(safe_args)

        try:
            result = subprocess.run(
                cmd_list,
                shell=False,  # Don't use shell
                capture_output=True,
                text=True,
                timeout=30,  # Set timeout
                check=True
            )
            return result.stdout
        except subprocess.CalledProcessError as e:
            print(f"Command failed: {e}")
            return None
        except subprocess.TimeoutExpired:
            print("Command timed out")
            return None

    def _sanitize_argument(self, arg: str) -> str:
        """Sanitize argument"""
        # Remove dangerous characters
        dangerous_chars = [';', '&', '|', '`', '$', '(', ')', '<', '>', '\\']
        safe_arg = arg
        for char in dangerous_chars:
            safe_arg = safe_arg.replace(char, '')
        return safe_arg

    def execute_safe_shell_command(self, command_line: str) -> Optional[str]:
        """Safely execute shell command"""
        try:
            # Use shlex to safely parse command line
            args = shlex.split(command_line)
            if not args:
                return None

            command = args[0]
            cmd_args = args[1:] if len(args) > 1 else []

            return self.execute_command(command, cmd_args)
        except ValueError as e:
            print(f"Invalid command line: {e}")
            return None

Logging Security

# W1203: logging-fstring-interpolation - Using f-string in logging
import logging

def bad_logging_practice():
    """Bad logging practice"""
    user_id = 12345
    action = "login"

    # Problem: f-string is executed even when log level doesn't match
    logging.debug(f"User {user_id} performed {action}")  # pylint: disable=logging-fstring-interpolation

# Fix solution: Use logging formatting
def good_logging_practice():
    """Good logging practice"""
    user_id = 12345
    action = "login"

    # Correct: Only formats when log level matches
    logging.debug("User %s performed %s", user_id, action)

# Safe logger
class SecurityLogger:
    """Security logger"""

    def __init__(self, name: str):
        self.logger = logging.getLogger(name)
        self.setup_logger()

    def setup_logger(self):
        """Set up logger"""
        if not self.logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter(
                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
            )
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)
            self.logger.setLevel(logging.INFO)

    def log_user_action(self, user_id: str, action: str, details: dict = None):
        """Log user action"""
        # Sanitize sensitive information
        safe_details = self._sanitize_log_data(details) if details else {}

        self.logger.info(
            "User action: user_id=%s, action=%s, details=%s",
            self._sanitize_user_id(user_id),
            action,
            safe_details
        )

    def log_security_event(self, event_type: str, source_ip: str, details: dict = None):
        """Log security event"""
        safe_details = self._sanitize_log_data(details) if details else {}

        self.logger.warning(
            "Security event: type=%s, source_ip=%s, details=%s",
            event_type,
            source_ip,
            safe_details
        )

    def _sanitize_user_id(self, user_id: str) -> str:
        """Sanitize user ID (mask)"""
        if len(user_id) > 4:
            return user_id[:2] + '*' * (len(user_id) - 4) + user_id[-2:]
        return '*' * len(user_id)

    def _sanitize_log_data(self, data: dict) -> dict:
        """Sanitize log data"""
        sensitive_keys = ['password', 'token', 'secret', 'api_key', 'credit_card']
        safe_data = {}

        for key, value in data.items():
            if any(sensitive in key.lower() for sensitive in sensitive_keys):
                safe_data[key] = '[REDACTED]'
            else:
                safe_data[key] = str(value)[:100]  # Limit length

        return safe_data

Random Numbers and Cryptographic Security

# B311: random-used - Using insecure random
import random

def bad_random_usage():
    """Insecure random usage"""
    # Not suitable for security-related purposes
    token = random.randint(100000, 999999)  # pylint: disable=random-used
    return str(token)

# Fix solution: Use cryptographically secure random
import secrets
import string
import hashlib
import os

class SecureRandomGenerator:
    """Secure random generator"""

    @staticmethod
    def generate_token(length: int = 32) -> str:
        """Generate secure random token"""
        return secrets.token_urlsafe(length)

    @staticmethod
    def generate_password(length: int = 12) -> str:
        """Generate secure random password"""
        alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
        password = ''.join(secrets.choice(alphabet) for _ in range(length))
        return password

    @staticmethod
    def generate_session_id() -> str:
        """Generate session ID"""
        random_data = secrets.token_bytes(32)
        timestamp = str(__import__('time').time()).encode()
        combined = random_data + timestamp
        return hashlib.sha256(combined).hexdigest()

    @staticmethod
    def generate_csrf_token() -> str:
        """Generate CSRF token"""
        return secrets.token_hex(32)

    @staticmethod
    def generate_otp(length: int = 6) -> str:
        """Generate one-time password"""
        digits = string.digits
        return ''.join(secrets.choice(digits) for _ in range(length))

# Password hashing and verification
import bcrypt

class PasswordManager:
    """Password manager"""

    @staticmethod
    def hash_password(password: str) -> str:
        """Hash password"""
        # Use bcrypt for secure hashing
        salt = bcrypt.gensalt(rounds=12)
        hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
        return hashed.decode('utf-8')

    @staticmethod
    def verify_password(password: str, hashed: str) -> bool:
        """Verify password"""
        try:
            return bcrypt.checkpw(
                password.encode('utf-8'),
                hashed.encode('utf-8')
            )
        except Exception:
            return False

    @staticmethod
    def is_strong_password(password: str) -> tuple[bool, list[str]]:
        """Check password strength"""
        issues = []

        if len(password) < 8:
            issues.append("Password must be at least 8 characters")

        if not any(c.islower() for c in password):
            issues.append("Must contain lowercase letters")

        if not any(c.isupper() for c in password):
            issues.append("Must contain uppercase letters")

        if not any(c.isdigit() for c in password):
            issues.append("Must contain digits")

        if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password):
            issues.append("Must contain special characters")

        return len(issues) == 0, issues

Input Validation and Sanitization

import re
from html import escape
from urllib.parse import quote

class InputValidator:
    """Input validator"""

    @staticmethod
    def validate_email(email: str) -> bool:
        """Validate email format"""
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return bool(re.match(pattern, email))

    @staticmethod
    def validate_phone(phone: str) -> bool:
        """Validate phone number"""
        # Simple phone number validation
        pattern = r'^\+?[1-9]\d{1,14}$'
        cleaned_phone = re.sub(r'[^\d+]', '', phone)
        return bool(re.match(pattern, cleaned_phone))

    @staticmethod
    def sanitize_html(text: str) -> str:
        """Sanitize HTML content"""
        return escape(text)

    @staticmethod
    def sanitize_sql_like(text: str) -> str:
        """Sanitize SQL LIKE query"""
        # Escape SQL LIKE wildcards
        text = text.replace('\\', '\\\\')
        text = text.replace('%', '\\%')
        text = text.replace('_', '\\_')
        return text

    @staticmethod
    def validate_filename(filename: str) -> tuple[bool, str]:
        """Validate filename safety"""
        if not filename:
            return False, "Filename cannot be empty"

        # Check for dangerous characters
        dangerous_chars = ['/', '\\', '..', '<', '>', ':', '"', '|', '?', '*']
        for char in dangerous_chars:
            if char in filename:
                return False, f"Filename contains dangerous character: {char}"

        # Check length
        if len(filename) > 255:
            return False, "Filename too long"

        # Check reserved names (Windows)
        reserved_names = [
            'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
            'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
            'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
        ]
        name_without_ext = filename.split('.')[0].upper()
        if name_without_ext in reserved_names:
            return False, f"Filename uses reserved name: {name_without_ext}"

        return True, "Filename is valid"

    @staticmethod
    def clean_user_input(text: str, max_length: int = 1000) -> str:
        """Clean user input"""
        if not isinstance(text, str):
            return ""

        # Remove control characters
        text = ''.join(char for char in text if ord(char) >= 32 or char in '\n\r\t')

        # Limit length
        if len(text) > max_length:
            text = text[:max_length]

        # Remove leading and trailing whitespace
        text = text.strip()

        return text
Exception Handling Best Practices
  1. Specific Catching: Catch specific exception types instead of using bare except
  2. Exception Chain: Use raise…from to preserve original exception information
  3. Resource Management: Use try-finally or context managers to ensure resource release
  4. Logging: Properly log exception information for debugging and monitoring
  5. Failure Strategy: Provide appropriate recovery strategies for different exception types
Security Precautions
  1. Input Validation: Strictly validate all external input
  2. Encoding Security: Explicitly specify file encoding to avoid cross-platform issues
  3. Process Execution: Avoid using shell=True, carefully handle user input
  4. Random Security: Use cryptographically secure random number generators
  5. Sensitive Information: Avoid logging sensitive information

Exception handling and security checks are important foundations for building robust and secure applications. Pylint’s related checks help identify potential security risks and code defects.