Python Security — Secure Coding, Input Validation, and Cryptography
In this tutorial, you will learn about Python Security. We cover key concepts, practical examples, and best practices to help you master this topic.
Security is not optional — it's a fundamental part of software development. Python's readability and extensive library support make it easier to write secure code, but the language itself won't protect you from common vulnerabilities. You need to understand the risks and apply defensive patterns.
In this tutorial, you'll learn Python security from a developer's perspective. DodaTech builds security tools, so these patterns are part of our everyday work — from validating user input in web applications to encrypting sensitive configuration data.
What You'll Learn
- Input validation and sanitization
- SQL Injection prevention
- Command Injection prevention
- Password hashing with bcrypt/argon2
- Encryption with cryptography library
- Secure configuration handling
- Common Python vulnerability patterns
Input Validation
Never trust user input. Validate everything:
import re
def validate_email(email):
"""Validate email format."""
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, email))
def validate_age(age_str):
"""Validate and convert age to integer."""
try:
age = int(age_str)
if 0 <= age <= 150:
return age
raise ValueError("Age out of range")
except ValueError:
raise ValueError("Age must be a valid number")
def sanitize_filename(filename):
"""Remove path traversal and dangerous characters."""
# Remove directory components
filename = filename.replace("/", "").replace("\\", "")
# Remove dangerous characters
filename = re.sub(r"[^\w\-.]", "", filename)
return filename
SQL Injection Prevention
import sqlite3
# VULNERABLE: string formatting
def get_user_bad(user_id):
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
# If user_id = "1 OR 1=1", returns ALL users!
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
return cursor.fetchall()
# SECURE: parameterized query
def get_user_safe(user_id):
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return cursor.fetchall()
# For dynamic table names (cannot use parameterization):
# Validate against a whitelist
VALID_TABLES = {"users", "posts", "comments"}
def query_table(table_name):
if table_name not in VALID_TABLES:
raise ValueError(f"Invalid table: {table_name}")
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table_name}")
return cursor.fetchall()
Command Injection Prevention
import subprocess
import shlex
# VULNERABLE: os.system with user input
def ping_host_bad(hostname):
# If hostname = "; rm -rf /", disaster!
import os
os.system(f"ping -c 1 {hostname}")
# SECURE: use subprocess with list arguments
def ping_host_safe(hostname):
result = subprocess.run(
["ping", "-c", "1", hostname],
capture_output=True,
text=True,
timeout=10
)
return result.returncode
# For shell=True (avoid if possible):
def run_shell_safe(user_input):
# Validate input first
allowed = re.match(r"^[a-zA-Z0-9_\-. ]+$", user_input)
if not allowed:
raise ValueError("Invalid characters in input")
result = subprocess.run(
f"echo {shlex.quote(user_input)}",
shell=True,
capture_output=True,
text=True
)
return result.stdout.strip()
Password Hashing
import hashlib
import os
# BAD: plain text or simple hash
def store_password_bad(password):
# Never do this!
return hashlib.md5(password.encode()).hexdigest()
# BAD: SHA-256 without salt
def store_password_better(password):
# Still vulnerable to rainbow tables
return hashlib.sha256(password.encode()).hexdigest()
# GOOD: use hashlib with salt
def hash_password(password):
"""Hash password with PBKDF2."""
salt = os.urandom(32) # 32 bytes of random salt
hash_bytes = hashlib.pbkdf2_hmac(
"sha256",
password.encode(),
salt,
100000 # 100k iterations
)
return salt + hash_bytes # Store salt + hash together
def verify_password(password, stored):
"""Verify password against stored hash."""
salt = stored[:32]
hash_bytes = stored[32:]
new_hash = hashlib.pbkdf2_hmac(
"sha256", password.encode(), salt, 100000
)
return new_hash == hash_bytes
# Test
hashed = hash_password("MySecureP@ss123")
print(f"Stored hash length: {len(hashed)} bytes")
print(f"Verified: {verify_password('MySecureP@ss123', hashed)}")
print(f"Wrong password: {verify_password('wrong', hashed)}")
Encryption with cryptography Library
from cryptography.fernet import Fernet
# Generate a key (store this securely!)
key = Fernet.generate_key()
cipher = Fernet(key)
# Encrypt data
message = b"API key: sk-1234-abcd-5678"
encrypted = cipher.encrypt(message)
print(f"Encrypted: {encrypted}")
# Decrypt data
decrypted = cipher.decrypt(encrypted)
print(f"Decrypted: {decrypted.decode()}")
# Key management for real applications
# Store the key in environment variables, not in code!
import os
os.environ["ENCRYPTION_KEY"] = key.decode()
loaded_key = os.environ["ENCRYPTION_KEY"].encode()
loaded_cipher = Fernet(loaded_key)
Secure Configuration Handling
import os
from dotenv import load_dotenv
# NEVER hardcode secrets!
# BAD:
DB_PASSWORD = "supersecret123"
API_KEY = "sk-abc123"
# GOOD: environment variables
load_dotenv() # Load .env file
DB_PASSWORD = os.getenv("DB_PASSWORD")
API_KEY = os.getenv("API_KEY")
if not DB_PASSWORD:
raise ValueError("DB_PASSWORD not set in environment")
# .env file (never commit this to git!)
# DB_PASSWORD=supersecret123
# API_KEY=sk-abc123
Common Vulnerability Patterns
Pickle Deserialization
import pickle
# DANGEROUS: Never unpickle untrusted data
class Evil:
def __reduce__(self):
import os
return (os.system, ("rm -rf /",))
# If someone sends you this pickle:
# pickle.loads(evil_pickle) # Would execute rm -rf / !
# SAFE alternatives:
# - Use json for data exchange
# - Use safepickle for controlled environments
eval() and exec()
# DANGEROUS: Never use eval() on user input
user_input = "__import__('os').system('rm -rf /')"
# result = eval(user_input) # Would execute!
# SAFE alternatives:
# - ast.literal_eval() for safe evaluation of literals
import ast
data = ast.literal_eval("[1, 2, 3]") # Safe
# data = ast.literal_eval("__import__('os')") # Raises ValueError
Path Traversal
import os
# VULNERABLE: allows path traversal
def read_file_bad(filename):
with open(f"data/{filename}") as f:
return f.read()
# read_file_bad("../../etc/passwd") reads system file!
# SECURE: validate resolved path
def read_file_safe(filename):
base_dir = os.path.abspath("data")
filepath = os.path.abspath(os.path.join(base_dir, filename))
if not filepath.startswith(base_dir):
raise PermissionError("Access denied")
with open(filepath) as f:
return f.read()
Practice Questions
Write a function that validates a password: minimum 8 chars, at least one uppercase, one lowercase, one digit, one special character.
Implement a rate limiter that allows at most 5 requests per minute per IP address.
Write a secure file upload handler that validates file type by content (not extension) and limits file size.
Implement AES encryption for large files (chunked encryption with cryptography library).
Write a function that sanitizes user input for log messages (prevent log injection — fake log entries).
Challenge: Secure Configuration Manager
Build a class that manages application secrets:
- Encrypts secrets at rest using Fernet
- Loads from environment variables, .env file, or encrypted config file
- Decrypts on demand with automatic cache timeout
- Logs access attempts (but not the secrets themselves!)
- Prevents secrets from appearing in tracebacks and log files
DodaTech's configuration system uses this exact pattern to manage database passwords, API keys, and TLS certificates across thousands of deployments.
Real-World Task: Log Sanitizer
Write a function that sanitizes log output before writing. It should:
- Mask credit card numbers (show only last 4 digits)
- Mask email addresses (show only domain: user@*****.com)
- Mask IP addresses (show only first two octets: 192.168..)
- Strip passwords from URLs and form data
- Replace session tokens and API keys with [REDACTED]
This is critical for Compliance with PCI-DSS, GDPR, and SOC 2 — all standards that DodaTech's security platform helps customers meet.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro