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
| Principle | Description | Pylint Check |
|---|---|---|
| Specific Catching | Catch specific exception types | broad-except |
| Exception Chain | Preserve original exception information | raise-missing-from |
| Resource Cleanup | Ensure resources are properly released | - |
| Logging | Log 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
- Specific Catching: Catch specific exception types instead of using bare except
- Exception Chain: Use raise…from to preserve original exception information
- Resource Management: Use try-finally or context managers to ensure resource release
- Logging: Properly log exception information for debugging and monitoring
- Failure Strategy: Provide appropriate recovery strategies for different exception types
Security Precautions
- Input Validation: Strictly validate all external input
- Encoding Security: Explicitly specify file encoding to avoid cross-platform issues
- Process Execution: Avoid using shell=True, carefully handle user input
- Random Security: Use cryptographically secure random number generators
- 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.