Security Engineering
A deep dive into the defensive engineering practices I apply across all projects. OWASP Top 10 mitigations, input validation, authentication, rate limiting, secrets management, and secure code patterns.
OWASP Top 10 Mitigations
I treat the OWASP Top 10 as a checklist for every project. Below are the specific mitigations I apply.
Every protected route checks JWT claims. Role-based access (USER, ADMIN, MODERATOR) is enforced at the middleware level. Direct object references use CUIDs (not sequential IDs) and ownership validation.
Passwords hashed with PBKDF2-SHA256 (600,000 iterations). JWT secrets loaded from environment variables. TLS enforced everywhere. No sensitive data in logs or error responses.
All SQL uses parameterised queries (Prisma / SQLite ? placeholders).
User input is validated with Pydantic models. HTML output is escaped by default
in Jinja2 templates.
Nginx adds security headers: X-Frame-Options, X-Content-Type-Options,
Referrer-Policy. Debug mode disabled in production. Error pages don't leak stack traces.
Output encoding by default. User content is escaped before rendering.
No innerHTML usage with untrusted data. Content Security Policy headers
restrict inline scripts.
All authentication events are logged (login success, failure, token refresh). Failed login attempts tracked per IP. Logs are structured JSON for easy search and alerting.
SQL Injection Prevention
Every database query uses parameterised statements. No string concatenation for SQL, ever. Here's the difference:
# NEVER DO THIS : vulnerable to injection query = f"SELECT * FROM users WHERE email = '{email}'" cursor.execute(query) # Attacker: ' OR 1=1 --
# Always use parameterised queries cursor.execute( "SELECT * FROM users WHERE email = ?", (email,) ) # Input is escaped by the driver
XSS Protection
Cross-site scripting is prevented at multiple layers: input validation, output encoding, and Content Security Policy.
from markupsafe import escape def render_comment(user_input: str) -> str: """Escape user input before rendering in HTML.""" # escape() converts <script> → <script> return f"<p class='comment'>{escape(user_input)}</p>"
// NEVER: element.innerHTML = untrustedData; // ALWAYS: use textContent for user-generated content const el = document.createElement("span"); el.textContent = userInput; // Auto-escaped by the browser container.appendChild(el);
- Input validation : reject invalid data at the boundary (Pydantic, regex)
- Output encoding : escape HTML entities before rendering (Jinja2 auto-escape,
textContent) - CSP headers :
Content-Security-Policyrestricts script sources - HTTPOnly cookies : tokens in cookies that JavaScript cannot read
CSRF Protection
For API services using JWT in the Authorization header, CSRF isn't a risk (the token isn't sent automatically by the browser). For cookie-based auth, I use the double-submit cookie pattern.
Set a random CSRF token in a non-HTTPOnly cookie. Every state-changing request must include this token in a custom header. The server compares cookie value with header value : a cross-origin page can't read the cookie to forge the header.
JWT sent in the Authorization: Bearer header is not auto-attached
by the browser, so CSRF attacks can't work. The attacker would need to steal
the token itself : which is mitigated by HTTPOnly storage and short TTLs.
Input Validation Patterns
All user input is validated at the API boundary before reaching business logic. Strict typing, length limits, regex patterns, and domain-specific constraints.
from pydantic import BaseModel, Field, field_validator import re class CreateLinkRequest(BaseModel): url: str = Field( ..., min_length=10, max_length=2048, pattern=r"^https?://", ) custom_code: str | None = Field( None, min_length=3, max_length=20, pattern=r"^[a-zA-Z0-9_-]+$", # Safe chars only ) @field_validator("url") def no_private_urls(cls, v): """Reject URLs pointing to internal networks.""" from urllib.parse import urlparse host = urlparse(v).hostname or "" if host in ("localhost", "127.0.0.1") or host.startswith("192.168."): raise ValueError("Private URLs not allowed") return v
Authentication Flow
Complete login → token → refresh → logout flow. See also: API Engineering → Authentication.
Client sends email + password over TLS.
Server verifies with constant-time comparison (hmac.compare_digest).
Issues JWT pair on success.
Access token (15 min TTL) in Authorization header. Contains user ID and role in claims. Verified by middleware on every protected request.
When access token expires, client sends refresh token (7 day TTL). Server issues a new access token and rotates the refresh token. Old refresh token is invalidated.
import hashlib, os def hash_password(password: str) -> str: """PBKDF2-SHA256 with per-user salt. Returns salt:hash.""" salt = os.urandom(32) key = hashlib.pbkdf2_hmac( "sha256", password.encode(), salt, iterations=600_000, ) return salt.hex() + ":" + key.hex() def verify_password(password: str, stored: str) -> bool: """Constant-time comparison to prevent timing attacks.""" salt_hex, key_hex = stored.split(":") salt = bytes.fromhex(salt_hex) key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 600_000) import hmac return hmac.compare_digest(key.hex(), key_hex)
Rate Limiting
Per-IP rate limiting on all API endpoints prevents abuse. Sensitive endpoints (login, registration) have stricter limits.
| Endpoint | Limit | Window | Reason |
|---|---|---|---|
| POST /login | 5 | 60s | Brute force prevention |
| POST /register | 3 | 60s | Account spam prevention |
| POST /shorten | 30 | 60s | API abuse prevention |
| GET /api/* | 60 | 60s | General rate protection |
import time from collections import defaultdict class RateLimiter: def __init__(self, max_requests: int, window: int): self.max = max_requests self.window = window self.hits = defaultdict(list) def is_allowed(self, key: str) -> bool: now = time.monotonic() # Remove expired entries self.hits[key] = [t for t in self.hits[key] if now - t < self.window] if len(self.hits[key]) >= self.max: return False self.hits[key].append(now) return True
Secrets Management
No secrets in code, no secrets in frontend, no secrets in logs. Everything sensitive is loaded from environment variables.
JWT_SECRET, DATABASE_URL, API keys : all loaded from
os.environ. Fail loudly at startup if missing.
.env, *.pem, *.key : all excluded from version control.
pre-commit hook scans for accidental secret commits.
Logs never contain passwords, tokens, or keys.
Request logging strips the Authorization header before writing.
import os, sys REQUIRED_ENV = ["JWT_SECRET", "DATABASE_URL"] def validate_config(): """Fail fast if required secrets are missing.""" missing = [k for k in REQUIRED_ENV if not os.environ.get(k)] if missing: print(f"FATAL: Missing environment variables: {missing}", file=sys.stderr) sys.exit(1) validate_config() # Called at import time