Skip to content

Python Security — Secure Coding, Input Validation, and Cryptography

DodaTech Updated 2026-06-29 5 min read

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

  1. Write a function that validates a password: minimum 8 chars, at least one uppercase, one lowercase, one digit, one special character.

  2. Implement a rate limiter that allows at most 5 requests per minute per IP address.

  3. Write a secure file upload handler that validates file type by content (not extension) and limits file size.

  4. Implement AES encryption for large files (chunked encryption with cryptography library).

  5. 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