# Owasp — Patterns
# OWASP Web Application Security
## When to Use
- Security code reviews
- Implementing authentication or authorization
- Handling user input from untrusted sources
- Building or auditing web API endpoints
- Configuring CORS, CSP, or other security headers
- Managing secrets, tokens, or credentials in code
- Setting up rate limiting or brute force protection
## When NOT to Use
- General code style or formatting reviews with no security implications
- Non-web applications such as CLI tools, batch scripts, or desktop utilities
- Performance optimization tasks where security is not the concern
- Infrastructure-level security (firewall rules, network segmentation)
---
## Core Patterns
### 1. Input Validation & Sanitization
Always validate input at the boundary. Use allowlists over denylists.
**Python (Pydantic)**
```python
# BAD - no validation, accepts anything
@app.post("/users")
async def create_user(request: Request):
data = await request.json()
name = data["name"] # no length check, no type check
email = data["email"] # no format validation
role = data["role"] # user controls their own role
db.execute(f"INSERT INTO users VALUES ('{name}', '{email}', '{role}')")
# GOOD - strict schema validation with Pydantic
from pydantic import BaseModel, EmailStr, Field
from enum import Enum
class UserRole(str, Enum):
viewer = "viewer"
editor = "editor"
class CreateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=100, pattern=r"^[a-zA-Z\s\-]+$")
email: EmailStr
role: UserRole = UserRole.viewer # default to least privilege
@app.post("/users")
async def create_user(payload: CreateUserRequest):
# Pydantic rejects invalid data before this code runs
db.add(User(name=payload.name, email=payload.email, role=payload.role))
```
**TypeScript (Zod)**
```typescript
// BAD - trusting req.body directly
app.post("/users", (req, res) => {
const { name, email, role } = req.body; // no validation
db.query(`INSERT INTO users VALUES ('${name}', '${email}', '${role}')`);
});
// GOOD - validate with Zod at the boundary
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s\-]+$/),
email: z.string().email(),
role: z.enum(["viewer", "editor"]).default("viewer"),
});
app.post("/users", (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
// result.data is typed and validated
await prisma.user.create({ data: result.data });
});
```
**File Upload Validation**
```python
# GOOD - validate MIME type (not just extension), size, and sanitize filename
import magic
ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
def validate_upload(file_bytes: bytes, filename: str) -> bool:
if len(file_bytes) > MAX_SIZE:
raise ValueError("File too large")
if magic.from_buffer(file_bytes, mime=True) not in ALLOWED_TYPES:
raise ValueError("Disallowed file type")
if ".." in filename or filename.startswith("."):
raise ValueError("Invalid filename")
return True
```
### 2. SQL Injection Prevention
Never concatenate user input into SQL strings. Always use parameterized queries or ORM methods.
**Raw SQL (Python)**
```python
# BAD - string interpolation creates injection vector
def get_user(user_id: str):
query = f"SELECT * FROM users WHERE id = '{user_id}'"
# Input: "'; DROP TABLE users; --" destroys the table
cursor.execute(query)
# GOOD - parameterized query
def get_user(user_id: str):
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return cursor.fetchone()
```
**SQLAlchemy (Python)**
```python
# BAD - text() with f-string
from sqlalchemy import text
result = session.execute(text(f"SELECT * FROM users WHERE name = '{name}'"))
# GOOD - bound parameters with text()
result = session.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})
# GOOD - ORM query (automatically parameterized)
user = session.query(User).filter(User.name == name).first()
```
**Prisma (TypeScript)**
```typescript
// BAD - raw query with interpolation
const user = await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = '${id}'`);
// GOOD - tagged template (auto-parameterized)
const user = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${id}`;
// GOOD - Prisma client methods (always safe)
const user = await prisma.user.findUnique({ where: { id } });
```
### 3. XSS Prevention
Prevent cross-site scripting by encoding output, setting CSP headers, and sanitizing HTML.
**Output Encoding**
```typescript
// BAD - renders raw user content as HTML
element.innerHTML = userComment;
// GOOD - use textContent for plain text
element.textContent = userComment;
// GOOD - React auto-escapes by default (don't bypass it)
return
{userComment}
;
// BAD - dangerouslySetInnerHTML defeats React's protection
return ;
```
**Sanitizing HTML When You Must Render It**
```typescript
// GOOD - sanitize with DOMPurify when HTML rendering is required
import DOMPurify from "dompurify";
const cleanHtml = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
ALLOWED_ATTR: ["href", "title"],
});
return ;
```
### 4. Authentication Patterns
**Password Hashing**
```python
# BAD - plain text or weak hashing
hashed = hashlib.md5(password.encode()).hexdigest() # trivially crackable
# GOOD - use argon2 (preferred) or bcrypt with proper cost
from passlib.hash import argon2
hashed = argon2.hash(password)
is_valid = argon2.verify(password, hashed)
```
```typescript
// GOOD - bcrypt in Node.js
import bcrypt from "bcrypt";
const SALT_ROUNDS = 12;
const hashed = await bcrypt.hash(password, SALT_ROUNDS);
const isValid = await bcrypt.compare(password, hashed);
```
**JWT Best Practices**
```python
# BAD - long-lived token, weak secret
token = jwt.encode({"user_id": 1, "exp": datetime.utcnow() + timedelta(days=365)},
"secret123", algorithm="HS256")
# GOOD - short expiry, strong secret, httpOnly cookie delivery
ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
def create_access_token(user_id: int) -> str:
return jwt.encode(
{"sub": user_id, "exp": datetime.now(timezone.utc) + ACCESS_TOKEN_EXPIRY},
os.environ["JWT_SECRET_KEY"], algorithm="HS256",
)
def set_token_cookie(response: Response, token: str):
response.set_cookie(
key="access_token", value=token,
httponly=True, secure=True, samesite="lax", # not accessible via JS, HTTPS only
max_age=int(ACCESS_TOKEN_EXPIRY.total_seconds()),
)
```
**Session Management Rules**
- Set session timeouts (30 minutes idle, 8 hours absolute)
- Regenerate session ID after login to prevent session fixation
- Store sessions server-side (Redis, database), not in cookies
- Clear sessions on logout (`request.session.clear()`)
- Use `httponly`, `secure`, and `samesite=lax` on session cookies
### 5. Authorization & Access Control
**RBAC Pattern**
```python
# GOOD - role-based access control with decorator
from enum import Enum
class Role(str, Enum):
admin = "admin"
editor = "editor"
viewer = "viewer"
ROLE_HIERARCHY = {Role.admin: 3, Role.editor: 2, Role.viewer: 1}
def require_role(minimum_role: Role):
def decorator(func):
async def wrapper(request: Request, *args, **kwargs):
user = request.state.user
if ROLE_HIERARCHY.get(user.role, 0) < ROLE_HIERARCHY[minimum_role]:
raise HTTPException(status_code=403)
return await func(request, *args, **kwargs)
return wrapper
return decorator
@app.delete("/posts/{post_id}")
@require_role(Role.editor)
async def delete_post(request: Request, post_id: int): ...
```
**Middleware-Based Authorization (Express)**
```typescript
// GOOD - authorization middleware
function requireRole(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
app.delete("/posts/:id", requireRole("admin", "editor"), deletePostHandler);
```
**Object-Level Permissions**
```python
# BAD - checks auth but not ownership (any user can edit any document)
@app.put("/documents/{doc_id}")
async def update_document(doc_id: int, payload: UpdateDoc, user=Depends(get_current_user)):
doc = await db.get(Document, doc_id)
doc.content = payload.content
# GOOD - verify ownership or admin role on every mutation
@app.put("/documents/{doc_id}")
async def update_document(doc_id: int, payload: UpdateDoc, user=Depends(get_current_user)):
doc = await db.get(Document, doc_id)
if not doc:
raise HTTPException(status_code=404)
if doc.owner_id != user.id and user.role != Role.admin:
raise HTTPException(status_code=403)
doc.content = payload.content
```
### 6. CORS Configuration
**FastAPI**
```python
# BAD - allows everything
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
allow_methods=["*"], allow_headers=["*"])
# GOOD - restrictive CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com", "https://staging.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
```
**Express**
```typescript
// BAD
app.use(cors({ origin: true, credentials: true }));
// GOOD - explicit allowlist with callback
const ALLOWED_ORIGINS = ["https://app.example.com"];
app.use(cors({
origin: (origin, cb) => {
if (!origin || ALLOWED_ORIGINS.includes(origin)) cb(null, true);
else cb(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
}));
```
### 7. Security Headers
**Express with Helmet**
```typescript
// GOOD - Helmet sets secure defaults for all critical headers
import helmet from "helmet";
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
frameAncestors: ["'none'"],
},
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
}));
```
**FastAPI**
```python
# GOOD - security headers middleware
@app.middleware("http")
async def security_headers(request, call_next):
response = await call_next(request)
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
response.headers["Content-Security-Policy"] = "default-src 'self'; frame-ancestors 'none';"
return response
```
### 8. Secret Management
```python
# BAD - hardcoded secrets
DATABASE_URL = "postgresql://admin:p@ssw0rd@localhost/mydb"
API_KEY = "sk-1234567890abcdef"
JWT_SECRET = "mysecret"
# GOOD - environment variables with validation
import os
def get_required_env(key: str) -> str:
value = os.environ.get(key)
if not value:
raise RuntimeError(f"Required environment variable {key} is not set")
return value
DATABASE_URL = get_required_env("DATABASE_URL")
API_KEY = get_required_env("API_KEY")
JWT_SECRET = get_required_env("JWT_SECRET")
```
**.env and .gitignore**
```bash
# .env (NEVER commit this file)
DATABASE_URL=postgresql://admin:securepass@localhost/mydb
JWT_SECRET=a-very-long-random-string-from-openssl-rand
API_KEY=sk-prod-xxxxxxxxxxxx
```
```gitignore
# .gitignore - always include these
.env
.env.*
!.env.example
*.pem
*.key
credentials.json
```
Commit a `.env.example` with empty values to document required variables without exposing secrets.
### 9. Rate Limiting
**Python (FastAPI with slowapi)**
```python
# GOOD - rate limiting on sensitive endpoints
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.post("/login")
@limiter.limit("5/minute") # brute force protection
async def login(request: Request, credentials: LoginRequest):
...
@app.post("/api/data")
@limiter.limit("100/minute") # general API rate limit
async def get_data(request: Request):
...
```
**Express (express-rate-limit)**
```typescript
// GOOD - tiered rate limiting
import rateLimit from "express-rate-limit";
const generalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 });
app.use("/api/", generalLimiter);
app.use("/auth/login", authLimiter);
app.use("/auth/register", authLimiter);
```
### 10. Dependency Security
```bash
# Python - audit dependencies
pip install pip-audit
pip-audit # scan for known vulnerabilities
pip-audit --fix # auto-fix where possible
# Node.js - audit dependencies
npm audit # list vulnerabilities
npm audit fix # auto-fix compatible updates
pnpm audit # pnpm equivalent
# Always commit lock files to ensure reproducible builds
# Python: requirements.txt or poetry.lock
# Node.js: package-lock.json, pnpm-lock.yaml, or yarn.lock
```
Run `npm audit --audit-level=high` and `pip-audit --strict` in CI (e.g., GitHub Actions on every PR and weekly schedule). Treat high-severity findings as build failures.
---
## Best Practices
1. **Validate at the boundary, trust nothing inside.** Every piece of user input -- query params, headers, request bodies, file uploads -- must be validated before processing. Use Pydantic or Zod schemas, not manual checks.
2. **Apply the principle of least privilege everywhere.** Default to the most restrictive access. Grant permissions explicitly. Use role-based access control and verify object-level ownership on every mutation.
3. **Never store or log secrets in plain text.** Use environment variables, a secret manager, or encrypted storage. Ensure secrets never appear in logs, error messages, or version control.
4. **Use strong, adaptive password hashing.** Always use argon2 or bcrypt with a sufficient work factor. Never use MD5, SHA-1, or SHA-256 alone for password storage.
5. **Set security headers on every response.** Enable HSTS, CSP, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy. Use Helmet for Express and middleware for FastAPI.
6. **Fail closed, not open.** When authentication or authorization checks encounter errors, deny access by default. Never fall through to an unprotected code path on exception.
7. **Keep dependencies updated and audited.** Run `npm audit` and `pip-audit` in CI pipelines. Pin dependency versions with lock files. Review changelogs before major upgrades.
8. **Enforce rate limiting on all public-facing endpoints.** Apply stricter limits on authentication and password reset endpoints. Use IP-based and account-based limiting together for defense in depth.
---
## Common Pitfalls
1. **Trusting client-side validation alone.** Attackers bypass browser validation trivially. Always re-validate on the server.
2. **Using wildcard CORS with credentials.** `allow_origins=["*"]` with credentials is insecure and browsers reject it. Specify exact origins.
3. **Storing JWTs in localStorage.** Any XSS can steal them. Use httpOnly, secure, sameSite cookies instead.
4. **Returning detailed error messages in production.** Stack traces help attackers. Return generic messages to clients, log details server-side.
5. **Using ORM raw query methods unsafely.** `$queryRawUnsafe` and `text()` with f-strings bypass ORM protections. Audit every raw SQL call.
6. **Checking authentication but not authorization.** "Logged in" does not mean "authorized." Check object-level permissions on every write.
7. **Disabling security in dev and shipping it.** CSP, CORS, HTTPS disabled for convenience can reach production. Use environment-aware config.
8. **Ignoring dependency vulnerabilities.** Known CVEs in transitive deps are a top attack vector. Automate auditing in CI.
---
## Security Review Checklist
- [ ] All user input validated with schema (Pydantic / Zod) before processing
- [ ] No string concatenation or interpolation in SQL queries
- [ ] Passwords hashed with argon2 or bcrypt (never MD5/SHA)
- [ ] JWTs have short expiry, use httpOnly cookies, strong secret from env
- [ ] Authorization checked at object level, not just authentication
- [ ] CORS configured with explicit origin allowlist (no wildcards with credentials)
- [ ] Security headers set: CSP, HSTS, X-Content-Type-Options, X-Frame-Options
- [ ] No secrets hardcoded in source -- all from environment variables
- [ ] .env files listed in .gitignore, .env.example committed
- [ ] Rate limiting applied to login, registration, and password reset endpoints
- [ ] File uploads validated by MIME type, size, and sanitized filename
- [ ] Error responses do not leak stack traces or internal details
- [ ] Dependencies audited with npm audit / pip-audit (no high-severity CVEs)
- [ ] HTTPS enforced in production with HSTS preload
- [ ] No use of eval(), dangerouslySetInnerHTML (without DOMPurify), or innerHTML
---
## Related Skills
- `docker` — Container security hardening
- `defense-in-depth` — Multi-layer security validation