Learn-skills.dev security-and-hardening
Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services.
install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/addyosmani/agent-skills/security-and-hardening" ~/.claude/skills/neversight-learn-skills-dev-security-and-hardening && rm -rf "$T"
manifest:
data/skills-md/addyosmani/agent-skills/security-and-hardening/SKILL.mdsource content
Security and Hardening
Overview
Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
When to Use
- Building anything that accepts user input
- Implementing authentication or authorization
- Storing or transmitting sensitive data
- Integrating with external APIs or services
- Adding file uploads, webhooks, or callbacks
- Handling payment or PII data
The Three-Tier Boundary System
Always Do (No Exceptions)
- Validate all external input at the system boundary (API routes, form handlers)
- Parameterize all database queries — never concatenate user input into SQL
- Encode output to prevent XSS (use framework auto-escaping, don't bypass it)
- Use HTTPS for all external communication
- Hash passwords with bcrypt/scrypt/argon2 (never store plaintext)
- Set security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- Use httpOnly, secure, sameSite cookies for sessions
- Run
(or equivalent) before every releasenpm audit
Ask First (Requires Human Approval)
- Adding new authentication flows or changing auth logic
- Storing new categories of sensitive data (PII, payment info)
- Adding new external service integrations
- Changing CORS configuration
- Adding file upload handlers
- Modifying rate limiting or throttling
- Granting elevated permissions or roles
Never Do
- Never commit secrets to version control (API keys, passwords, tokens)
- Never log sensitive data (passwords, tokens, full credit card numbers)
- Never trust client-side validation as a security boundary
- Never disable security headers for convenience
- Never use
oreval()
with user-provided datainnerHTML - Never store sessions in client-accessible storage (localStorage for auth tokens)
- Never expose stack traces or internal error details to users
OWASP Top 10 Prevention
1. Injection (SQL, NoSQL, OS Command)
// BAD: SQL injection via string concatenation const query = `SELECT * FROM users WHERE id = '${userId}'`; // GOOD: Parameterized query const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]); // GOOD: ORM with parameterized input const user = await prisma.user.findUnique({ where: { id: userId } });
2. Broken Authentication
// Password hashing import { hash, compare } from 'bcrypt'; const SALT_ROUNDS = 12; const hashedPassword = await hash(plaintext, SALT_ROUNDS); const isValid = await compare(plaintext, hashedPassword); // Session management app.use(session({ secret: process.env.SESSION_SECRET, // From environment, not code resave: false, saveUninitialized: false, cookie: { httpOnly: true, // Not accessible via JavaScript secure: true, // HTTPS only sameSite: 'lax', // CSRF protection maxAge: 24 * 60 * 60 * 1000, // 24 hours }, }));
3. Cross-Site Scripting (XSS)
// BAD: Rendering user input as HTML element.innerHTML = userInput; // GOOD: Use framework auto-escaping (React does this by default) return <div>{userInput}</div>; // If you MUST render HTML, sanitize first import DOMPurify from 'dompurify'; const clean = DOMPurify.sanitize(userInput);
4. Broken Access Control
// Always check authorization, not just authentication app.patch('/api/tasks/:id', authenticate, async (req, res) => { const task = await taskService.findById(req.params.id); // Check that the authenticated user owns this resource if (task.ownerId !== req.user.id) { return res.status(403).json({ error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' } }); } // Proceed with update const updated = await taskService.update(req.params.id, req.body); return res.json(updated); });
5. Security Misconfiguration
// Security headers (use helmet for Express) import helmet from 'helmet'; app.use(helmet()); // Content Security Policy app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], // Tighten if possible imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'"], }, })); // CORS — restrict to known origins app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000', credentials: true, }));
6. Sensitive Data Exposure
// Never return sensitive fields in API responses function sanitizeUser(user: UserRecord): PublicUser { const { passwordHash, resetToken, ...publicFields } = user; return publicFields; } // Use environment variables for secrets const API_KEY = process.env.STRIPE_API_KEY; if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
Input Validation Patterns
Schema Validation at Boundaries
import { z } from 'zod'; const CreateTaskSchema = z.object({ title: z.string().min(1).max(200).trim(), description: z.string().max(2000).optional(), priority: z.enum(['low', 'medium', 'high']).default('medium'), dueDate: z.string().datetime().optional(), }); // Validate at the route handler app.post('/api/tasks', async (req, res) => { const result = CreateTaskSchema.safeParse(req.body); if (!result.success) { return res.status(422).json({ error: { code: 'VALIDATION_ERROR', message: 'Invalid input', details: result.error.flatten(), }, }); } // result.data is now typed and validated const task = await taskService.create(result.data); return res.status(201).json(task); });
File Upload Safety
// Restrict file types and sizes const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']; const MAX_SIZE = 5 * 1024 * 1024; // 5MB function validateUpload(file: UploadedFile) { if (!ALLOWED_TYPES.includes(file.mimetype)) { throw new ValidationError('File type not allowed'); } if (file.size > MAX_SIZE) { throw new ValidationError('File too large (max 5MB)'); } // Don't trust the file extension — check magic bytes if critical }
Rate Limiting
import rateLimit from 'express-rate-limit'; // General API rate limit app.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window standardHeaders: true, legacyHeaders: false, })); // Stricter limit for auth endpoints app.use('/api/auth/', rateLimit({ windowMs: 15 * 60 * 1000, max: 10, // 10 attempts per 15 minutes }));
Secrets Management
.env files: ├── .env.example → Committed (template with placeholder values) ├── .env → NOT committed (contains real secrets) └── .env.local → NOT committed (local overrides) .gitignore must include: .env .env.local .env.*.local *.pem *.key
Always check before committing:
# Check for accidentally staged secrets git diff --cached | grep -i "password\|secret\|api_key\|token"
Security Review Checklist
### Authentication - [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12) - [ ] Session tokens are httpOnly, secure, sameSite - [ ] Login has rate limiting - [ ] Password reset tokens expire ### Authorization - [ ] Every endpoint checks user permissions - [ ] Users can only access their own resources - [ ] Admin actions require admin role verification ### Input - [ ] All user input validated at the boundary - [ ] SQL queries are parameterized - [ ] HTML output is encoded/escaped ### Data - [ ] No secrets in code or version control - [ ] Sensitive fields excluded from API responses - [ ] PII encrypted at rest (if applicable) ### Infrastructure - [ ] Security headers configured (CSP, HSTS, etc.) - [ ] CORS restricted to known origins - [ ] Dependencies audited for vulnerabilities - [ ] Error messages don't expose internals
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. |
| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. |
| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. |
| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. |
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
Red Flags
- User input passed directly to database queries, shell commands, or HTML rendering
- Secrets in source code or commit history
- API endpoints without authentication or authorization checks
- Missing CORS configuration or wildcard (
) origins* - No rate limiting on authentication endpoints
- Stack traces or internal errors exposed to users
- Dependencies with known critical vulnerabilities
Verification
After implementing security-relevant code:
-
shows no critical or high vulnerabilitiesnpm audit - No secrets in source code or git history
- All user input validated at system boundaries
- Authentication and authorization checked on every protected endpoint
- Security headers present in response (check with browser DevTools)
- Error responses don't expose internal details
- Rate limiting active on auth endpoints