DEV Community

Anna lilith
Anna lilith

Posted on

Python Security Best Practices: Protect Your Code and Data

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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

Browse the collection →

Top comments (0)