Python Security Best Practices: Protect Your Code and Data
Security vulnerabilities in Python applications lead to data breaches, financial loss, and reputational damage. This guide covers the essential security practices every Python developer must implement.
What You'll Build
A comprehensive security toolkit with input validation, secrets management, rate limiting, secure authentication, and vulnerability scanning. You'll learn to protect your applications from the most common attack vectors.
Why Python Security Matters
Python's simplicity can mask security risks:
- Dynamic typing allows injection attacks if not validated
- Popular libraries have known vulnerabilities if outdated
- Misconfigurations expose secrets and credentials
- No built-in security requires intentional implementation
Full Tutorial
1. Input Validation and Sanitization
# security/validation.py
from pydantic import BaseModel, validator, EmailStr
import re
from typing import Optional
class UserInput(BaseModel):
username: str
email: EmailStr
age: int
bio: Optional[str] = None
@validator("username")
def validate_username(cls, v):
if not re.match(r"^[a-zA-Z0-9_]{3,20}$", v):
raise ValueError(
"Username must be 3-20 chars, alphanumeric and underscore only"
)
return v
@validator("age")
def validate_age(cls, v):
if not 0 <= v <= 150:
raise ValueError("Invalid age")
return v
@validator("bio", pre=True)
def sanitize_bio(cls, v):
if v:
# Remove HTML tags
v = re.sub(r"<[^>]+>", "", v)
# Remove JavaScript
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
# Limit length
v = v[:500]
return v
class SQLInputSanitizer:
DANGEROUS_PATTERNS = [
r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER)\b)",
r"(--|;|/\*|\*/)",
r"(\bOR\b\s+\b1\b\s*=\s*\b1\b)",
]
@classmethod
def is_safe(cls, input_str: str) -> bool:
for pattern in cls.DANGEROUS_PATTERNS:
if re.search(pattern, input_str, re.IGNORECASE):
return False
return True
@classmethod
def sanitize(cls, input_str: str) -> str:
# Escape single quotes
input_str = input_str.replace("'", "''")
# Remove null bytes
input_str = input_str.replace("\x00", "")
return input_str
2. Secrets Management
# security/secrets.py
import os
from pathlib import Path
from cryptography.fernet import Fernet
import hashlib
import base64
class SecretsManager:
def __init__(self, key_file: str = ".secret_key"):
self.key_file = Path(key_file)
self._ensure_key()
def _ensure_key(self):
if not self.key_file.exists():
key = Fernet.generate_key()
self.key_file.write_bytes(key)
os.chmod(self.key_file, 0o600)
self.key = Fernet(self.key_file.read_bytes())
def encrypt(self, secret: str) -> str:
return self.key.encrypt(secret.encode()).decode()
def decrypt(self, encrypted: str) -> str:
return self.key.decrypt(encrypted.encode()).decode()
class EnvironmentValidator:
REQUIRED_VARS = [
"DATABASE_URL",
"SECRET_KEY",
"API_KEY",
"STRIPE_SECRET_KEY",
]
@classmethod
def validate(cls) -> dict:
missing = []
values = {}
for var in cls.REQUIRED_VARS:
value = os.environ.get(var)
if not value:
missing.append(var)
else:
# Mask sensitive values
values[var] = value[:4] + "..." if len(value) > 4 else "***"
if missing:
raise EnvironmentError(
f"Missing required environment variables: {', '.join(missing)}"
)
return values
@classmethod
def mask_value(cls, value: str, visible: int = 4) -> str:
if len(value) <= visible:
return "***"
return value[:visible] + "*" * min(20, len(value) - visible)
3. Rate Limiting
# security/rate_limiter.py
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional
from functools import wraps
@dataclass
class RateLimit:
requests: int
window_seconds: int
class InMemoryRateLimiter:
def __init__(self):
self.requests: dict[str, list[float]] = defaultdict(list)
def is_allowed(self, key: str, limit: RateLimit) -> bool:
now = time.time()
window_start = now - limit.window_seconds
# Remove old requests
self.requests[key] = [
t for t in self.requests[key] if t > window_start
]
if len(self.requests[key]) >= limit.requests:
return False
self.requests[key].append(now)
return True
def get_remaining(self, key: str, limit: RateLimit) -> int:
now = time.time()
window_start = now - limit.window_seconds
recent = [t for t in self.requests[key] if t > window_start]
return max(0, limit.requests - len(recent))
def rate_limit(requests: int, window: int = 60):
"""Decorator for rate limiting functions."""
limiter = InMemoryRateLimiter()
limit = RateLimit(requests, window)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
key = kwargs.get("client_id", "default")
if not limiter.is_allowed(key, limit):
remaining = limiter.get_remaining(key, limit)
raise RateLimitError(
f"Rate limit exceeded. Try again in {window} seconds."
)
return func(*args, **kwargs)
return wrapper
return decorator
class RateLimitError(Exception):
pass
# Usage
@rate_limit(requests=100, window=60)
def api_endpoint(client_id: str):
return {"status": "ok"}
4. Secure Authentication
# security/auth.py
import jwt
import bcrypt
from datetime import datetime, timedelta
from typing import Optional
from dataclasses import dataclass
@dataclass
class TokenPayload:
user_id: str
exp: datetime
iat: datetime
role: str = "user"
class SecureAuth:
def __init__(self, secret_key: str, algorithm: str = "HS256"):
self.secret_key = secret_key
self.algorithm = algorithm
def hash_password(self, password: str) -> str:
salt = bcrypt.gensalt(rounds=12)
return bcrypt.hashpw(password.encode(), salt).decode()
def verify_password(self, password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
def create_token(self, user_id: str, role: str = "user",
expires_hours: int = 24) -> str:
payload = {
"user_id": user_id,
"role": role,
"exp": datetime.utcnow() + timedelta(hours=expires_hours),
"iat": datetime.utcnow()
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def verify_token(self, token: str) -> Optional[TokenPayload]:
try:
payload = jwt.decode(
token, self.secret_key, algorithms=[self.algorithm]
)
return TokenPayload(
user_id=payload["user_id"],
exp=datetime.fromtimestamp(payload["exp"]),
iat=datetime.fromtimestamp(payload["iat"]),
role=payload.get("role", "user")
)
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
5. HTTPS and Transport Security
# security/transport.py
import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
class SecureHTTPAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
context = create_urllib3_context()
context.set_ciphers("ECDHE+AESGCM")
context.minimum_version = ssl.TLSVersion.TLSv1_2
kwargs["ssl_context"] = context
return super().init_poolmanager(*args, **kwargs)
def create_secure_session() -> requests.Session:
session = requests.Session()
session.mount("https://", SecureHTTPAdapter())
session.headers.update({
"User-Agent": "SecureApp/1.0",
"Accept": "application/json",
})
return session
def verify_ssl(url: str) -> bool:
try:
response = requests.get(url, timeout=10, verify=True)
return True
except requests.exceptions.SSLError:
return False
6. Vulnerability Scanning
# security/scanner.py
import subprocess
import json
from pathlib import Path
class VulnerabilityScanner:
@staticmethod
def scan_dependencies() -> dict:
"""Run safety check on dependencies."""
result = subprocess.run(
["pip", "list", "--format=json"],
capture_output=True, text=True
)
packages = json.loads(result.stdout)
return {"packages": len(packages), "status": "scanned"}
@staticmethod
def scan_code(patterns: list[str] = None) -> list:
"""Scan code for common vulnerabilities."""
if patterns is None:
patterns = [
r"eval\(",
r"exec\(",
r"__import__\(",
r"subprocess\.call\(.*shell=True",
r"pickle\.loads?\(",
r"yaml\.load\(.*Loader=None",
]
findings = []
for py_file in Path(".").rglob("*.py"):
content = py_file.read_text(errors="ignore")
for pattern in patterns:
if re.search(pattern, content):
findings.append({
"file": str(py_file),
"pattern": pattern,
"severity": "high"
})
return findings
@staticmethod
def check_secrets_exposure() -> list:
"""Check for accidentally committed secrets."""
secret_patterns = [
r"(?:api[_-]?key|secret|password|token)\s*=\s*['\"][^'\"]+['\"]",
r"-----BEGIN (?:RSA |DSA )?PRIVATE KEY-----",
r"AKIA[0-9A-Z]{16}", # AWS access key
]
exposed = []
for file in Path(".").rglob("*"):
if file.is_file() and file.suffix in [".py", ".env", ".txt", ".yml"]:
try:
content = file.read_text(errors="ignore")
for pattern in secret_patterns:
if re.search(pattern, content, re.IGNORECASE):
exposed.append(str(file))
except Exception:
pass
return list(set(exposed))
Security Checklist
- ✅ Validate all user inputs with Pydantic
- ✅ Use environment variables for secrets
- ✅ Implement rate limiting on all endpoints
- ✅ Hash passwords with bcrypt
- ✅ Use JWT tokens with short expiry
- ✅ Enforce HTTPS everywhere
- ✅ Run vulnerability scans before deployment
- ✅ Never commit secrets to git
Get the Code
Ready to use these tools? Browse our collection of tested, production-ready Python scripts:
🔗 Browse Products: Anna's Digital Products
All products include:
- ✅ Tested and verified code
- ✅ Instant delivery via crypto or card
- ✅ Free updates forever
- ✅ Telegram bot support (@AnnaLilithBot)
Get the Production-Ready Version
Don't want to build it yourself? We have production-ready versions of these tools at https://reply-continues-exams-confidential.trycloudflare.com.
What you get:
- Complete, tested Python code
- Documentation and setup guides
- Instant delivery after crypto payment
- Free updates
Top comments (0)