| name | secure-code-guardian |
| description | Use when implementing authentication/authorization, securing user input, or preventing OWASP Top 10 vulnerabilities. Invoke for authentication, authorization, input validation, encryption, OWASP Top 10 prevention. |
| triggers | ["security","authentication","authorization","encryption","OWASP","vulnerability","secure coding","password","JWT","OAuth"] |
| role | specialist |
| scope | implementation |
| output-format | code |
Secure Code Guardian
Security-focused developer specializing in writing secure code and preventing vulnerabilities.
Role Definition
You are a senior security engineer with 10+ years of application security experience. You specialize in secure coding practices, OWASP Top 10 prevention, and implementing authentication/authorization. You think defensively and assume all input is malicious.
When to Use This Skill
- Implementing authentication/authorization
- Securing user input handling
- Implementing encryption
- Preventing OWASP Top 10 vulnerabilities
- Security hardening existing code
- Implementing secure session management
Core Workflow
- Threat model - Identify attack surface and threats
- Design - Plan security controls
- Implement - Write secure code with defense in depth
- Validate - Test security controls
- Document - Record security decisions
Reference Guide
Load detailed guidance based on context:
| Topic | Reference | Load When |
|---|
| OWASP | references/owasp-prevention.md | OWASP Top 10 patterns |
| Authentication | references/authentication.md | Password hashing, JWT |
| Input Validation | references/input-validation.md | Zod, SQL injection |
| XSS/CSRF | references/xss-csrf.md | XSS prevention, CSRF |
| Headers | references/security-headers.md | Helmet, rate limiting |
Constraints
MUST DO
- Hash passwords with bcrypt/argon2 (never plaintext)
- Use parameterized queries (prevent SQL injection)
- Validate and sanitize all user input
- Implement rate limiting on auth endpoints
- Use HTTPS everywhere
- Set security headers
- Log security events
- Store secrets in environment/secret managers
MUST NOT DO
- Store passwords in plaintext
- Trust user input without validation
- Expose sensitive data in logs or errors
- Use weak encryption algorithms
- Hardcode secrets in code
- Disable security features for convenience
Output Templates
When implementing security features, provide:
- Secure implementation code
- Security considerations noted
- Configuration requirements (env vars, headers)
- Testing recommendations
Knowledge Reference
OWASP Top 10, bcrypt/argon2, JWT, OAuth 2.0, OIDC, CSP, CORS, rate limiting, input validation, output encoding, encryption (AES, RSA), TLS, security headers
Related Skills
- Fullstack Guardian - Feature implementation with security
- Security Reviewer - Security code review
- Architecture Designer - Security architecture
Authentication
Password Hashing
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/;
function validatePassword(password: string): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (password.length < 12) errors.push('Minimum 12 characters');
if (!/[a-z]/.test(password)) errors.push('Requires lowercase');
if (!/[A-Z]/.test(password)) errors.push('Requires uppercase');
if (!/\d/.test(password)) errors.push('Requires digit');
if (!/[@$!%*?&]/.test(password)) errors.push('Requires special character');
return { valid: errors.length === 0, errors };
}
JWT Implementation
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
interface TokenPayload {
sub: string;
type: 'access' | 'refresh';
}
function generateAccessToken(userId: string): string {
return jwt.sign(
{ sub: userId, type: 'access' },
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRY }
);
}
function generateRefreshToken(userId: string): string {
return jwt.sign(
{ sub: userId, type: 'refresh' },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
}
function verifyToken(token: string): TokenPayload {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
}
Auth Middleware
function authMiddleware(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
try {
const token = header.slice(7);
const payload = verifyToken(token);
if (payload.type !== 'access') {
return res.status(401).json({ error: 'Invalid token type' });
}
req.userId = payload.sub;
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
Account Lockout
const MAX_ATTEMPTS = 5;
const LOCKOUT_DURATION = 15 * 60 * 1000;
async function handleLoginAttempt(email: string, success: boolean) {
const key = `login:attempts:${email}`;
if (success) {
await redis.del(key);
return;
}
const attempts = await redis.incr(key);
await redis.expire(key, LOCKOUT_DURATION / 1000);
if (attempts >= MAX_ATTEMPTS) {
await redis.set(`login:locked:${email}`, '1', 'PX', LOCKOUT_DURATION);
throw new Error('Account locked. Try again later.');
}
}
Quick Reference
| Practice | Implementation |
|---|
| Password hash | bcrypt (12+ rounds) |
| Token expiry | Access: 15m, Refresh: 7d |
| Lockout | 5 attempts, 15min lockout |
| MFA | TOTP (authenticator apps) |
| JWT Claim | Purpose |
|---|
sub | User ID |
exp | Expiration |
iat | Issued at |
type | access/refresh |
Input Validation
Zod Validation
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100).regex(/^[\w\s-]+$/),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['user', 'admin']).default('user'),
});
function validateUser(data: unknown) {
return UserSchema.parse(data);
}
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error(result.error.issues);
}
SQL Injection Prevention
const bad = `SELECT * FROM users WHERE id = ${userId}`;
const bad2 = `SELECT * FROM users WHERE name = '${name}'`;
const good = await db.query(
'SELECT * FROM users WHERE id = $1 AND name = $2',
[userId, name]
);
const user = await prisma.user.findFirst({
where: { id: userId, name: name }
});
const user = await knex('users')
.where({ id: userId, name: name })
.first();
Path Traversal Prevention
import path from 'path';
const vulnerable = path.join('/uploads', userInput);
function getSecurePath(baseDir: string, userInput: string): string {
const sanitized = path.basename(userInput);
const fullPath = path.resolve(baseDir, sanitized);
if (!fullPath.startsWith(path.resolve(baseDir))) {
throw new Error('Invalid path');
}
return fullPath;
}
Command Injection Prevention
import { execFile } from 'child_process';
exec(`convert ${userInput}`);
execFile('convert', ['-resize', '100x100', safeFilename], (error, stdout) => {
});
import sharp from 'sharp';
await sharp(inputPath).resize(100, 100).toFile(outputPath);
URL Validation
function validateUrl(input: string, allowedHosts: string[]): URL {
const url = new URL(input);
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Invalid protocol');
}
if (!allowedHosts.includes(url.hostname)) {
throw new Error('Host not allowed');
}
return url;
}
File Upload Validation
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
const MAX_SIZE = 5 * 1024 * 1024;
function validateUpload(file: Express.Multer.File) {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new Error('Invalid file type');
}
if (file.size > MAX_SIZE) {
throw new Error('File too large');
}
const buffer = fs.readFileSync(file.path);
const type = fileType.fromBuffer(buffer);
if (!type || !ALLOWED_TYPES.includes(type.mime)) {
throw new Error('Invalid file content');
}
}
Quick Reference
| Input Type | Validation |
|---|
| Email | Regex + max length |
| URL | Protocol + host allowlist |
| File path | basename + resolve check |
| SQL | Parameterized queries |
| Command | execFile + no shell |
| File upload | Type + size + magic bytes |
OWASP Top 10 Prevention
OWASP Top 10 Quick Reference
| # | Vulnerability | Prevention |
|---|
| 1 | Injection | Parameterized queries, ORMs |
| 2 | Broken Auth | Strong passwords, MFA, secure sessions |
| 3 | Sensitive Data | Encryption at rest/transit |
| 4 | XXE | Disable DTDs, use JSON |
| 5 | Broken Access | Deny by default, server-side validation |
| 6 | Misconfig | Security headers, disable defaults |
| 7 | XSS | Output encoding, CSP |
| 8 | Insecure Deserialization | Schema validation, allowlists |
| 9 | Known Vulnerabilities | Dependency scanning |
| 10 | Insufficient Logging | Log security events |
A01: Injection Prevention
const bad = `SELECT * FROM users WHERE id = ${userId}`;
const good = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
const user = await prisma.user.findUnique({ where: { id: userId } });
exec(`ls ${userInput}`);
const files = fs.readdirSync(safeDirectory);
A02: Broken Authentication
const hash = await bcrypt.hash(password, 12);
const isValid = await bcrypt.compare(password, hash);
if (failedAttempts >= 5) {
await lockAccount(userId, 15 * 60 * 1000);
}
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
},
}));
A03: Sensitive Data Exposure
import crypto from 'crypto';
function encrypt(text: string, key: Buffer): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
}
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(`https://${req.hostname}${req.url}`);
}
next();
});
A05: Broken Access Control
async function getResource(userId: string, resourceId: string) {
const resource = await db.resource.findUnique({ where: { id: resourceId } });
if (resource.ownerId !== userId) {
throw new ForbiddenError('Access denied');
}
return resource;
}
function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
A07: XSS Prevention
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
}));
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
Quick Reference
| Attack | Defense |
|---|
| SQL Injection | Parameterized queries |
| XSS | Output encoding, CSP |
| CSRF | CSRF tokens |
| IDOR | Authorization checks |
| Command Injection | Avoid exec(), validate input |
Security Headers
Helmet (Express)
import helmet from 'helmet';
app.use(helmet());
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
}));
Manual Headers
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
});
Rate Limiting
import rateLimit from 'express-rate-limit';
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: 'Too many requests' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', apiLimiter);
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Too many login attempts' },
skipSuccessfulRequests: true,
});
app.post('/api/login', authLimiter, loginHandler);
app.post('/api/register', authLimiter, registerHandler);
CORS Configuration
import cors from 'cors';
app.use(cors({
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
}));
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://example.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
}));
Cookie Security
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 900000,
path: '/',
domain: '.example.com',
});
Quick Reference
| Header | Value | Purpose |
|---|
| X-Frame-Options | DENY | Clickjacking |
| X-Content-Type-Options | nosniff | MIME sniffing |
| Strict-Transport-Security | max-age=31536000 | Force HTTPS |
| Content-Security-Policy | default-src 'self' | XSS |
| Referrer-Policy | strict-origin-when-cross-origin | Privacy |
| Cookie Flag | Purpose |
|---|
| httpOnly | No JS access |
| secure | HTTPS only |
| sameSite=strict | CSRF protection |
| maxAge | Expiration |
XSS & CSRF Prevention
XSS Prevention
Output Encoding
function SafeComponent({ userInput }: { userInput: string }) {
return <div>{userInput}</div>;
}
import DOMPurify from 'dompurify';
function HtmlContent({ html }: { html: string }) {
return (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(html)
}}
/>
);
}
Content Security Policy
import helmet from 'helmet';
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
upgradeInsecureRequests: [],
},
}));
Input Sanitization
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty);
const cleanStrict = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href'],
});
const textOnly = DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [] });
CSRF Prevention
Synchronizer Token Pattern
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
});
Double Submit Cookie
res.cookie('csrf', token, {
httpOnly: false,
secure: true,
sameSite: 'strict',
});
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrf'),
},
});
if (req.cookies.csrf !== req.headers['x-csrf-token']) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
SameSite Cookies
app.use(session({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
},
}));
HTTP Headers
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
Quick Reference
| Attack | Prevention |
|---|
| Reflected XSS | Output encoding |
| Stored XSS | Input sanitization + encoding |
| DOM XSS | Avoid innerHTML, use textContent |
| CSRF | Tokens + SameSite cookies |
| Header | Purpose |
|---|
| CSP | Script/resource restrictions |
| X-Frame-Options | Clickjacking |
| X-Content-Type-Options | MIME sniffing |
| SameSite | CSRF protection |