Patterns — Securing Backend Services
Contents
- Security-headers middleware
- SSRF-safe URL fetcher
- CSRF setup
- Problem+json error mapper
- Secrets loading
- Dependency scanning in CI
- Gotchas
Security-headers middleware
python
SECURITY_HEADERS = {
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Content-Type-Options": "nosniff",
"Content-Security-Policy": "default-src 'self'; frame-ancestors 'none'",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Cache-Control": "no-store", # for API responses carrying user data
}
@app.middleware("http")
async def add_security_headers(request, call_next):
resp = await call_next(request)
for k, v in SECURITY_HEADERS.items():
resp.headers.setdefault(k, v) # setdefault: edge/proxy may own some
return respSSRF-safe URL fetcher
python
import ipaddress, socket
from urllib.parse import urlparse
ALLOWED_HOSTS = {"api.partner.example"} # allowlist beats any blocklist
def assert_public(host: str):
for info in socket.getaddrinfo(host, None):
ip = ipaddress.ip_address(info[4][0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise ValueError(f"blocked address {ip}")
def safe_fetch(url: str):
p = urlparse(url)
if p.scheme != "https" or p.hostname not in ALLOWED_HOSTS:
raise ValueError("destination not allowed")
assert_public(p.hostname) # resolve NOW, at request time
return requests.get(url, timeout=10, allow_redirects=False) # re-check per hop if followingCSRF setup
python
# Use the framework middleware — do not hand-roll token comparison.
# Django: CsrfViewMiddleware (default). Flask: flask-wtf CSRFProtect(app).
# FastAPI cookie-auth: double-submit cookie via starlette-csrf.
# Token in a custom header (X-CSRF-Token) read from a non-HttpOnly cookie;
# SameSite=Lax remains as defense-in-depth, not the defense.Problem+json error mapper
python
@app.exception_handler(Exception)
async def unhandled(request, exc):
cid = request.state.correlation_id
logger.exception("unhandled error cid=%s", cid) # full detail → logs
return JSONResponse(status_code=500, content={ # zero detail → client
"type": "about:blank", "title": "Internal error",
"status": 500, "correlation_id": cid,
})Secrets loading
python
import os
class Settings:
def __init__(self):
self.db_url = self._req("DATABASE_URL")
self.signing_key = self._req("SIGNING_KEY")
@staticmethod
def _req(name: str) -> str:
val = os.environ.get(name)
if not val:
raise RuntimeError(f"missing required secret {name}") # fail fast, no defaults
return valPre-commit guard: gitleaks protect --staged blocks accidental secret commits.
Dependency scanning in CI
yaml
# GitHub Actions
- run: pip install pip-audit && pip-audit --strict # Python
- run: npm audit --audit-level=high # Node
# Fail the build on criticals; schedule a weekly run so quiet repos still alert.Gotchas
- HSTS on a domain still serving plain HTTP paths locks users out for max-age; deploy HTTPS fully first, then add the header, then preload.
- CSP
report-onlyleft on forever — attackers are unaffected by reports; graduate to enforcing after a week of clean reports. - SSRF via DNS rebinding — validate the resolved IP at request time (as above), not in a separate pre-check the attacker can race.
allow_redirects=Trueafter an allowlist check — the first hop is allowed, the redirect goes to the metadata IP; disable or re-validate per hop.- Rotating a secret without invalidating derived artifacts — old JWTs signed with the leaked key stay valid; rotate key AND revoke issued tokens.
- One shared "backend" IAM role — a compromise anywhere is a compromise everywhere; one identity per service.
- Error middleware ordered after routers — exceptions in earlier middleware bypass the mapper; register it outermost.