| name | shieldcode |
| description | Security hardening and production-grade error handling for all code Claude generates. Auto-activates when writing code that handles user input, database queries, authentication, API endpoints, file operations, error handling, or logging. Prevents OWASP Top 10 vulnerabilities and enforces secure defaults across JavaScript/TypeScript and Python.
|
ShieldCode - Security & Error Handling
You MUST follow these rules when generating ANY code. These are non-negotiable constraints, not suggestions.
PART 1: SECURITY RULES
1. Input Validation
Rule: ALWAYS validate and sanitize ALL user input before processing. Use allowlist validation (define what IS allowed), never blocklist. Validate type, length, format, and range. Never trust client-side validation alone.
app.post('/user', async (req, res) => {
const { username, age, role } = req.body;
await db.query(`INSERT INTO users VALUES ('${username}', ${age}, '${role}')`);
});
import { z } from 'zod';
const CreateUserSchema = z.object({
username: z.string().min(3).max(32).regex(/^[a-zA-Z0-9_]+$/),
age: z.number().int().min(13).max(120),
role: z.enum(['user', 'moderator']),
});
app.post('/user', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ message: 'Invalid input', errors: result.error.flatten() });
}
const { username, age, role } = result.data;
await db.query('INSERT INTO users (username, age, role) VALUES ($1, $2, $3)', [username, age, role]);
});
@app.post("/user")
async def create_user(request: Request):
data = await request.json()
username = data["username"]
age = data["age"]
role = data["role"]
await db.execute(f"INSERT INTO users VALUES ('{username}', {age}, '{role}')")
from pydantic import BaseModel, Field, field_validator
from enum import Enum
import re
class UserRole(str, Enum):
user = "user"
moderator = "moderator"
class CreateUserRequest(BaseModel):
username: str = Field(min_length=3, max_length=32)
age: int = Field(ge=13, le=120)
role: UserRole
@field_validator("username")
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not re.match(r'^[a-zA-Z0-9_]+$', v):
ValueError()
v
():
db.execute(
,
body.username, body.age, body.role.value
)
2. SQL / NoSQL Injection Prevention
Rule: ALWAYS use parameterized queries or prepared statements. NEVER concatenate user input into query strings. This applies to ALL databases: PostgreSQL, MySQL, SQLite, MongoDB.
const userId = req.params.id;
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
const userId = req.params.id;
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
const search = req.query.search as string;
const sanitizedSearch = search.replace(/[%_\\]/g, '\\$&');
const results = await db.query(
'SELECT * FROM products WHERE name ILIKE $1',
[`%${sanitizedSearch}%`]
);
const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
user_id = request.path_params["id"]
user = await db.fetchrow(f"SELECT * FROM users WHERE id = {user_id}")
user_id = request.path_params["id"]
user = await db.fetchrow("SELECT * FROM users WHERE id = $1", int(user_id))
from sqlalchemy import select
stmt = select(User).where(User.id == int(user_id))
result = await session.execute(stmt)
user = result.scalar_one_or_none()
3. XSS Prevention
Rule: ALWAYS escape output rendered in HTML. Use Content-Security-Policy headers. Never assign user-controlled data to innerHTML, outerHTML, document.write, or similar DOM sinks. Use textContent instead. In React, never use the dangerouslySetInnerHTML prop without sanitization.
const username = searchParams.get('name');
document.getElementById('greeting').innerHTML = `Hello, ${username}!`;
const username = searchParams.get('name') ?? '';
document.getElementById('greeting').textContent = `Hello, ${username}!`;
function Comment({ text }: { text: string }) {
return <div>{text}</div>;
}
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(trustedHtmlContent);
from jinja2 import Environment
env = Environment(autoescape=False)
template = env.from_string("<p>Hello {{ name }}</p>")
from jinja2 import Environment
env = Environment(autoescape=True)
template = env.from_string("<p>Hello {{ name }}</p>")
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
Always set these security response headers:
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
4. Authentication
Rule: ALWAYS use bcrypt or argon2 for password hashing. NEVER use md5, sha1, sha256, or any fast hash for passwords. ALWAYS validate JWT signature, expiration, AND issuer. ALWAYS rate-limit auth endpoints. Use secure cookie attributes.
import crypto from 'crypto';
const hash = crypto.createHash('sha256').update(password).digest('hex');
import jwt from 'jsonwebtoken';
const payload = jwt.decode(token);
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
const hash = await bcrypt.hash(password, SALT_ROUNDS);
const isValid = await bcrypt.compare(password, hash);
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'],
issuer: 'my-app',
audience: 'my-app-users',
}) as JwtPayload;
res.cookie('session', token, {
httpOnly: ,
: process.. === ,
: ,
: * * ,
});
rateLimit ;
authLimiter = ({
: * * ,
: ,
: { : },
: ,
: ,
});
app.(, authLimiter, loginHandler);
import hashlib
hashed = hashlib.sha256(password.encode()).hexdigest()
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
is_valid = bcrypt.checkpw(password.encode(), hashed)
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
hashed = ph.hash(password)
try:
ph.verify(hashed, password)
is_valid = True
except Exception:
is_valid = False
from jose import jwt, JWTError
from datetime import datetime, timezone
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
settings.JWT_SECRET,
algorithms=["HS256"],
options={"require": ["exp", "iat", "sub"]}
)
if payload["exp"] < datetime.now(timezone.utc).timestamp():
raise ValueError("Token expired")
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
from slowapi Limiter
slowapi.util get_remote_address
limiter = Limiter(key_func=get_remote_address)
():
...
5. Authorization
Rule: ALWAYS check permissions server-side. Never trust client-provided role or permission fields. Validate resource ownership before allowing access or modification. Apply principle of least privilege.
app.post('/admin/action', async (req, res) => {
if (req.body.role === 'admin') {
await performAdminAction();
}
});
app.delete('/post/:id', authenticate, async (req, res) => {
await db.query('DELETE FROM posts WHERE id = $1', [req.params.id]);
});
app.post('/admin/action', authenticate, requireRole('admin'), async (req, res) => {
await performAdminAction();
});
function requireRole(role: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (req.user?.role !== role) {
return res.status(403).({ : });
}
();
};
}
app.(, authenticate, (req, res) => {
post = db.(, [req..]);
(!post.[]) res.().({ : });
(post.[]. !== req..) {
res.().({ : });
}
db.(, [req.., req..]);
res.().();
});
@app.delete("/post/{post_id}")
async def delete_post(post_id: int, current_user: User = Depends(get_current_user)):
await db.execute("DELETE FROM posts WHERE id = $1", post_id)
@app.delete("/post/{post_id}", status_code=204)
async def delete_post(
post_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
post = await db.get(Post, post_id)
if post is None:
raise HTTPException(status_code=404, detail="Post not found")
if post.author_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
await db.delete(post)
await db.commit()
6. Secrets and Configuration
Rule: NEVER hardcode secrets, API keys, passwords, or tokens in source code. ALWAYS use environment variables or a secret manager. NEVER log secrets. NEVER include secrets in error messages or API responses.
const JWT_SECRET = 'super-secret-key-123';
const DB_URL = 'postgres://admin:password123@localhost/mydb';
const client = new OpenAI({ apiKey: 'sk-proj-abc123...' });
import { z } from 'zod';
const EnvSchema = z.object({
JWT_SECRET: z.string().min(32),
DATABASE_URL: z.string().url(),
OPENAI_API_KEY: z.string().startsWith('sk-'),
NODE_ENV: z.enum(['development', 'production', 'test']),
});
const env = EnvSchema.parse(process.env);
catch (error) {
res.status(500).json({ error: error.message, config: process.env });
}
(error) {
logger.(, { : error. });
res.().({ : });
}
JWT_SECRET = "super-secret-key"
API_KEY = "sk-proj-abc123"
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
jwt_secret: str
database_url: str
openai_api_key: str
environment: str = "development"
@field_validator("jwt_secret")
@classmethod
def validate_secret_length(cls, v: str) -> str:
if len(v) < 32:
raise ValueError("JWT secret must be at least 32 characters")
return v
settings = Settings()
7. File Operations
Rule: ALWAYS validate file paths to prevent path traversal. Restrict upload file types and sizes. Never execute uploaded files. Resolve paths and confirm they are within the intended directory.
app.get('/files/:filename', (req, res) => {
const filePath = path.join('/uploads', req.params.filename);
res.sendFile(filePath);
});
import path from 'path';
import fs from 'fs';
const UPLOAD_DIR = path.resolve('/uploads');
app.get('/files/:filename', (req, res) => {
const requestedPath = path.resolve(UPLOAD_DIR, req.params.filename);
if (!requestedPath.startsWith(UPLOAD_DIR + path.sep)) {
return res.status(400).json({ message: 'Invalid file path' });
}
if (!fs.existsSync(requestedPath)) {
return res.status(404).json({ message: });
}
res.(requestedPath);
});
multer ;
= [, , , ];
= * * ;
upload = ({
: multer.({ : }),
: { : },
: {
(.(file.)) {
(, );
} {
( ());
}
},
});
@app.get("/files/{filename}")
async def get_file(filename: str):
file_path = f"/uploads/{filename}"
return FileResponse(file_path)
import os
from pathlib import Path
from fastapi import HTTPException
from fastapi.responses import FileResponse
UPLOAD_DIR = Path("/uploads").resolve()
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"}
MAX_FILE_SIZE = 5 * 1024 * 1024
@app.get("/files/{filename}")
async def get_file(filename: str):
requested = (UPLOAD_DIR / filename).resolve()
if not str(requested).startswith(str(UPLOAD_DIR) + "/"):
raise HTTPException(status_code=400, detail="Invalid file path")
if not requested.exists():
raise HTTPException(status_code=404, detail="File not found")
FileResponse(requested)
():
ext = Path(file.filename ).suffix.lower()
ext ALLOWED_EXTENSIONS:
HTTPException(status_code=, detail=)
content = file.read(MAX_FILE_SIZE + )
(content) > MAX_FILE_SIZE:
HTTPException(status_code=, detail=)
8. Dependency Safety (Slopsquatting Prevention)
Rule: NEVER suggest packages you are not certain exist. Do not invent package names. Verify package names before recommending. Prefer well-known, actively maintained packages.
Trusted packages (TypeScript/Node):
- Password hashing:
bcrypt, argon2
- JWT:
jsonwebtoken, jose
- Validation:
zod, joi, yup
- Rate limiting:
express-rate-limit, rate-limiter-flexible
- HTML sanitization:
dompurify, sanitize-html
- File upload:
multer
- Security headers:
helmet
Trusted packages (Python):
- Password hashing:
bcrypt, argon2-cffi
- JWT:
python-jose[cryptography], pyjwt
- Validation:
pydantic (built-in to FastAPI)
- Rate limiting:
slowapi, limits
- HTML sanitization:
bleach, nh3
- Settings:
pydantic-settings
PART 2: ERROR HANDLING RULES
1. The Golden Rules
Never expose internal details to clients. Never swallow exceptions silently. Always log enough context to debug without logging sensitive data.
try {
await processPayment(order);
} catch (e) {}
try {
await processPayment(order);
} catch (e) {
res.status(500).json({ error: e.stack });
}
try {
await chargeCard(cardNumber, cvv, amount);
} catch (e) {
logger.error('Payment failed', { cardNumber, cvv, amount, error: e });
}
try {
await processPayment(order);
} catch (error) {
if (error instanceof PaymentDeclinedError) {
return res.status(402).json({ message: error.userMessage });
}
if (error instanceof ValidationError) {
return res.status(400).json({ message: error.message });
}
logger.error('Payment processing failed', {
: order.,
: order.,
: order.,
: error..,
: error.,
});
res.().({ : });
}
try:
await process_payment(order)
except Exception:
pass
try:
await process_payment(order)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
try:
await process_payment(order)
except PaymentDeclinedError as e:
raise HTTPException(status_code=402, detail=e.user_message)
except ValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(
"Payment processing failed",
extra={
"order_id": order.id,
"user_id": order.user_id,
"amount": str(order.amount),
"error_type": type(e).__name__,
"error_message": str(e),
}
)
raise HTTPException(status_code=500, detail="Payment processing failed. Please try again.")
2. Custom Error Classes
Rule: Define typed error classes for different failure modes. This enables precise handling at boundaries and consistent error responses.
class AppError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly userMessage: string = message,
public readonly code?: string
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message: string) { super(message, 400); }
}
class NotFoundError extends AppError {
constructor(resource: string) { super(` not found`, ); }
}
{
() { (, , ); }
}
{
() { (, , ); }
}
app.( {
(error ) {
res.(error.).({ : error. });
}
logger.(, {
: req.,
: req.,
: error..,
: error.,
});
res.().({ : });
});
class AppError(Exception):
def __init__(self, message: str, status_code: int, user_message: str | None = None):
super().__init__(message)
self.status_code = status_code
self.user_message = user_message or message
class ValidationError(AppError):
def __init__(self, message: str):
super().__init__(message, 400)
class NotFoundError(AppError):
def __init__(self, resource: str):
super().__init__(f"{resource} not found", 404)
class UnauthorizedError(AppError):
def __init__(self):
super().__init__("Unauthorized", 401, "Authentication required")
class ForbiddenError(AppError):
():
().__init__(, , )
fastapi Request
fastapi.responses JSONResponse
():
JSONResponse(status_code=exc.status_code, content={: exc.user_message})
():
logger.error(, extra={: request.url.path, : (exc)})
JSONResponse(status_code=, content={: })
3. HTTP Status Codes
Rule: Use the correct HTTP status code for every response. NEVER return 200 for an error. NEVER return 500 for a validation error.
| Code | When to use |
|---|
| 200 | Successful GET, PUT, PATCH |
| 201 | Successful POST that created a resource |
| 204 | Successful DELETE or action with no response body |
| 400 | Bad Request: malformed request, wrong types |
| 401 | Unauthenticated: no valid credentials provided |
| 403 | Authenticated but not authorized for this resource |
| 404 | Resource not found |
| 409 | Conflict: duplicate resource, state conflict |
| 422 | Unprocessable Entity: valid format but semantic error |
| 429 | Too Many Requests: rate limit exceeded |
| 500 | Internal Server Error: unexpected failure only |
| 503 | Service Unavailable: downstream dependency down |
app.post('/users', async (req, res) => {
if (!req.body.email) {
return res.status(200).json({ error: 'Email required' });
}
const existing = await findUserByEmail(req.body.email);
if (existing) {
return res.status(500).json({ error: 'User exists' });
}
const user = await createUser(req.body);
return res.status(200).json(user);
});
app.post('/users', async (req, res) => {
if (!req.body.email) {
return res.status(400).json({ message: 'Email is required' });
}
const existing = await (req..);
(existing) {
res.().({ : });
}
user = (req.);
res.().(user);
});
4. Structured Logging
Rule: Use structured logging with JSON format. Include correlation IDs. NEVER log passwords, tokens, API keys, credit card numbers, or PII. Use appropriate log levels.
console.log(`User ${req.body.email} logged in with password ${req.body.password}`);
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
redact: ['password', 'token', 'authorization', 'cookie', '*.password', '*.token'],
});
import { randomUUID } from 'crypto';
app.use((req, res, next) => {
req.requestId = randomUUID();
next();
});
logger.info({ userId: user.id, event: 'user.login', requestId: req.requestId }, 'User logged in');
logger.error({ orderId, : error.., : req. }, );
import logging
logging.info(f"User {email} logged in with password {password}")
import structlog
import uuid
log = structlog.get_logger()
log.info("user.login", user_id=user.id, request_id=request_id)
log.error("order.failed", order_id=order_id, error_type=type(e).__name__, request_id=request_id)
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = str(uuid.uuid4())
structlog.contextvars.bind_contextvars(request_id=request_id)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
structlog.contextvars.clear_contextvars()
return response
5. External Calls: Timeouts, Retries, Circuit Breakers
Rule: ALL external calls (HTTP, DB, cache, queues) MUST have timeouts. Implement retry with exponential backoff for transient failures. Fail gracefully when a dependency is unavailable.
const response = await fetch('https://api.payment.com/charge', {
method: 'POST',
body: JSON.stringify(data),
});
async function callWithRetry<T>(
fn: (signal: AbortSignal) => Promise<T>,
options: { maxAttempts?: number; baseDelayMs?: number; timeoutMs?: number } = {}
): Promise<T> {
const { maxAttempts = 3, baseDelayMs = 200, timeoutMs = 5000 } = options;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await fn(controller.signal);
clearTimeout(timeout);
return result;
} catch (error) {
(timeout);
isLast = attempt === maxAttempts;
isRetryable = error || (error )?. >= ;
(isLast || !isRetryable) error;
delay = baseDelayMs * ** (attempt - ) + .() * ;
logger.(, { attempt, delay });
( (r, delay));
}
}
();
}
response = (
(, {
: ,
: { : },
: .(data),
signal,
})
);
import httpx
response = await client.post("https://api.payment.com/charge", json=data)
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import httpx
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=0.2, min=0.2, max=5),
retry=retry_if_exception_type((httpx.TimeoutException, httpx.ConnectError)),
reraise=True,
)
async def call_payment_api(data: dict) -> dict:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.post(
"https://api.payment.com/charge",
json=data,
headers={"Content-Type": "application/json"},
)
response.raise_for_status()
return response.json()
6. Async / Unhandled Rejections
Rule: ALWAYS handle promise rejections. Attach global unhandledRejection and uncaughtException handlers. In Python async code, always await coroutines and handle exceptions from background tasks.
sendWelcomeEmail(user.email);
app.get('/data', async (req, res) => {
processData(req.body);
res.json({ ok: true });
});
sendWelcomeEmail(user.email).catch(error => {
logger.error('Failed to send welcome email', { userId: user.id, error: error.message });
});
process.on('unhandledRejection', (reason) => {
logger.error('Unhandled promise rejection', { reason });
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught exception', { error: error.message, stack: error.stack });
process.exit(1);
});
asyncio.create_task(send_welcome_email(user.email))
import asyncio
def handle_task_error(task: asyncio.Task) -> None:
if not task.cancelled():
exc = task.exception()
if exc:
logger.error("Background task failed", extra={"error": str(exc)})
task = asyncio.create_task(send_welcome_email(user.email))
task.add_done_callback(handle_task_error)
from fastapi import BackgroundTasks
@app.post("/users")
async def create_user(body: CreateUserRequest, background_tasks: BackgroundTasks):
user = await user_service.create(body)
background_tasks.add_task(send_welcome_email, user.email)
return user
PART 3: QUICK REFERENCE CHECKLIST
Before finalizing any code that handles user data, run through this list:
Security:
Error Handling: