security-practices
Spardutti/claude-skills/skills/security-practices/SKILL.md
MUST USE when writing or reviewing code that handles user input, authentication, authorization, API endpoints, database queries, secrets, or any security-sensitive functionality. Enforces OWASP Top 10 prevention, secure defaults, and defense-in-depth patterns.
Skill3 starsChanged 33 days ago
- Reads credentials
- Deletes or force-pushes
What's in it
- Security Best Practices
- Input Validation — Never Trust the Client
- Allowlist Over Denylist
- SQL Injection Prevention
- XSS Prevention
- Security Headers
- Authentication
- Hash Passwords with bcrypt
- JWT — Short Expiry, Pinned Algorithm
- Generic Auth Errors
- Authorization
- Secrets
- Error Handling — Never Leak Internals
- CORS
- Cookies
- Rules
- Anti-Rationalizations
---
name: security-practices
metadata:
gate-paths: "**/*"
category: Foundations
description: "MUST USE when writing or reviewing code that handles user input, authentication, authorization, API endpoints, database queries, secrets, or any security-sensitive functionality. Enforces OWASP Top 10 prevention, secure defaults, and defense-in-depth patterns."
---
# Security Best Practices
## Input Validation — Never Trust the Client
```python
# BAD: no validation
@app.post("/users")
async def create_user(data: dict):
db.execute(f"INSERT INTO users (email) VALUES ('{data['email']}')")
# GOOD: strict schema + parameterized query
class UserCreate(BaseModel):
email: EmailStr
username: str = Field(min_length=3, max_length=64, pattern=r"^[a-zA-Z0-9_-]+$")
@app.post("/users")
async def create_user(data: UserCreate):
await user_service.create(data)
```
```typescript
// GOOD: validate with zod
const UserCreate = z.object({
email: z.string().email(),
username: z.string().min(3).max(64).regex(/^[a-zA-Z0-9_-]+$/),
});
app.post("/users", (req, res) => {
const data = UserCreate.parse(req.body);
});
```
### Allowlist Over Denylist
```python
# BAD: blocking known-bad input
for bad in ["<script>", "DROP TABLE"]:
value = value.replace(bad, "")
# GOOD: only allow known-good
class SortParams(BaseModel):
sort_by: Literal["created_at", "updated_at", "name", "price"]
order: Literal["asc", "desc"] = "asc"
```
## SQL Injection Prevention
```python
# BAD: string interpolation
query = f"SELECT * FROM users WHERE username = '{username}'"
# GOOD: parameterized
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
# GOOD: SQLAlchemy ORM — automatically parameterized
user = await db.execute(select(User).where(User.username == username))
```
```typescript
// BAD
const query = `SELECT * FROM users WHERE id = ${userId}`;
// GOOD
await db.query("SELECT * FROM users WHERE id = $1", [userId]);
```
Never interpolate column/table names from user input — validate against an allowlist.
## XSS Prevention
```python
# BAD: raw HTML
return HTMLResponse(f"<h1>Hello {name}</h1>")
# GOOD: auto-escaping templates (Jinja2)
return templates.TemplateResponse("greet.html", {"request": request, "name": name})
```
### Security Headers
```python
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = "default-src 'self'"
response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains"
```
```typescript
import helmet from "helmet";
app.use(helmet());
```
## Authentication
### Hash Passwords with bcrypt
```python
# BAD
hashed = hashlib.md5(password.encode()).hexdigest()
# GOOD
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
hashed = pwd_context.hash(password)
is_valid = pwd_context.verify(plain_password, hashed)
```
### JWT — Short Expiry, Pinned Algorithm
```python
# BAD: no expiry, allows "none" algorithm
token = jwt.encode({"user_id": 1}, "secret")
data = jwt.decode(token, "secret", algorithms=["HS256", "none"])
# GOOD
token = jwt.encode(
{"sub": str(user.id), "exp": datetime.now(timezone.utc) + timedelta(minutes=15)},
settings.jwt_secret, algorithm="HS256",
)
payload = jwt.decode(token, settings.jwt_secret, algorithms=["HS256"])
```
### Generic Auth Errors
```python
# BAD: reveals which field is wrong
if not user: raise HTTPException(400, "User not found")
if not verify_password(...): raise HTTPException(400, "Incorrect password")
# GOOD: prevents user enumeration
if not user or not verify_password(password, user.hashed_password):
raise HTTPException(401, "Incorrect username or password")
```
## Authorization
```python
# BAD: IDOR — any user can access any order
@router.get("/orders/{order_id}")
async def get_order(order_id: int, db: DBSession):
return await db.get(Order, order_id)
# GOOD: scope to authenticated user
@router.get("/orders/{order_id}")
async def get_order(order_id: int, db: DBSession, user: CurrentUser):
order = await db.execute(
select(Order).where(Order.id == order_id, Order.user_id == user.id)
)
if not (order := order.scalar_one_or_none()):
raise HTTPException(status_code=404)
return order
# Role-based access
def require_role(*roles: str):
async def check(current_user: CurrentUser):
if current_user.role not in roles:
raise HTTPException(status_code=403, detail="Forbidden")
return current_user
return check
AdminUser = Annotated[User, Depends(require_role("admin"))]
```
## Secrets
```python
# BAD: hardcoded
JWT_SECRET = "super-secret-key-123"
# GOOD: environment variables with validation
class Settings(BaseSettings):
jwt_secret: str
database_url: str
model_config = SettingsConfigDict(env_file=".env")
```
```gitignore
.env
*.pem
*.key
credentials.json
```
## Error Handling — Never Leak Internals
```python
# BAD
return JSONResponse(status_code=500, content={"detail": str(exc)})
# GOOD
logger.exception("Unhandled error", exc_info=exc)
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
```
## CORS
```python
# BAD: wildcard with credentials
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
# GOOD: explicit origins
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"],
allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"])
```
## Cookies
```python
response.set_cookie(key="session", value=token,
httponly=True, secure=True, samesite="lax", max_age=3600)
```
## Rules
1. **Validate all input server-side** — Pydantic, Zod, or equivalent
2. **Allowlist over denylist** — validate against known-good values
3. **Parameterize all queries** — never interpolate user input into SQL
4. **Escape all output** — auto-escaping templates, CSP header
5. **Hash passwords with bcrypt/argon2** — never MD5/SHA/plain text
6. **Pin JWT algorithm, set short expiry** — never allow `"none"`
7. **Generic auth errors** — never reveal which field was wrong
8. **Scope queries to authenticated user** — prevent IDOR
9. **Never hardcode secrets** — env variables, `.env` in `.gitignore`
10. **Never leak internal errors** — log details, return generic messages
11. **Explicit CORS origins** — never `"*"` with credentials
12. **Secure cookie flags** — `httponly`, `secure`, `samesite`
## Anti-Rationalizations
| Excuse | Rebuttal |
|--------|----------|
| "It's an internal tool, doesn't need validation" | Internal tools get exposed — VPN drops, SSRF, accidental public deploy. Validate at every boundary. |
| "The frontend already validates this" | Client validation is UX, not security. Anyone can hit the API directly with curl. |
| "We'll add auth before launch" | Auth bolted on late misses object-level checks (IDOR). Build it in from the first endpoint. |
| "String interpolation is fine, the input is trusted" | "Trusted" is a vibe, not a guarantee. Parameterize every query, every time — no exceptions. |
| "Logging the full error helps debugging" | And leaks stack traces, paths, and secrets to attackers. Log details server-side, return generic messages. |
| "We need CORS `*` for the public API" | Then it can't use credentials. `allow_origins=["*"]` + `allow_credentials=True` is a critical bug. |
| "MD5 is fine, we just need a quick hash" | Not for passwords, not for tokens, not for anything security-adjacent. Use bcrypt/argon2 or a real KDF. |
More agent context in Spardutti/claude-skills
16 other files this repository gives its agents.
CLAUDE.md
Skill
- code-structureskills/code-structure/SKILL.md
- debuggingskills/debugging/SKILL.md
- docker-best-practicesskills/docker-best-practices/SKILL.md
- drf-best-practicesskills/drf-best-practices/SKILL.md
- drizzle-ormskills/drizzle-orm/SKILL.md
- expressskills/express/SKILL.md
- fastapiskills/fastapi/SKILL.md
- git-merge-conflictsskills/git-merge-conflicts/SKILL.md
- reactskills/react/SKILL.md
- researchskills/research/SKILL.md
- sqlskills/sql/SKILL.md
- tanstack-queryskills/tanstack-query/SKILL.md
- tanstack-routerskills/tanstack-router/SKILL.md
- testing-best-practicesskills/testing-best-practices/SKILL.md
- typescript-best-practicesskills/typescript-best-practices/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

