security-expert
SKILL.md
Security Expert
Expert guidance for application security, vulnerability assessment, penetration testing, OWASP Top 10, secure coding practices, and security architecture.
Core Concepts
Security Principles
- Defense in depth
- Least privilege
- Secure by default
- Fail securely
- Complete mediation
- Separation of duties
- Zero trust architecture
OWASP Top 10 (2021)
- Broken Access Control
- Cryptographic Failures
- Injection
- Insecure Design
- Security Misconfiguration
- Vulnerable and Outdated Components
- Identification and Authentication Failures
- Software and Data Integrity Failures
- Security Logging and Monitoring Failures
- Server-Side Request Forgery (SSRF)
Security Domains
- Authentication & Authorization
- Cryptography
- Input validation
- Session management
- Error handling
- Secure communications
- Data protection
OWASP Top 10 Vulnerabilities
1. Broken Access Control
// ❌ Vulnerable: No authorization check
app.get('/api/users/:id/profile', async (req, res) => {
const profile = await db.users.findById(req.params.id);
res.json(profile);
});
// ✅ Secure: Verify user owns the resource
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const profile = await db.users.findById(req.params.id);
res.json(profile);
});
// ✅ Better: Use middleware
const authorizeResource = (resourceType) => async (req, res, next) => {
const resourceId = req.params.id;
const resource = await db[resourceType].findById(resourceId);
if (!resource) {
return res.status(404).json({ error: 'Not found' });
}
if (resource.userId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
req.resource = resource;
next();
};
app.delete('/api/posts/:id', authenticate, authorizeResource('posts'), async (req, res) => {
await req.resource.delete();
res.status(204).send();
});
2. Injection (SQL, NoSQL, Command)
// ❌ SQL Injection vulnerability
app.get('/users', (req, res) => {
const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;
db.query(query, (err, results) => {
res.json(results);
});
});
// Attack: ?name=' OR '1'='1
// ✅ Secure: Use parameterized queries
app.get('/users', async (req, res) => {
const results = await db.query(
'SELECT * FROM users WHERE name = ?',
[req.query.name]
);
res.json(results);
});
// ❌ Command Injection
const { exec } = require('child_process');
app.post('/convert', (req, res) => {
exec(`convert ${req.body.filename} output.pdf`, (err, stdout) => {
res.send(stdout);
});
});
// Attack: filename="; rm -rf / #"
// ✅ Secure: Use safe APIs, validate input
const { spawn } = require('child_process');
app.post('/convert', (req, res) => {
const filename = path.basename(req.body.filename); // Remove path traversal
if (!/^[a-zA-Z0-9_-]+\.(jpg|png)$/.test(filename)) {
return res.status(400).json({ error: 'Invalid filename' });
}
const process = spawn('convert', [filename, 'output.pdf']);
// Handle process output safely
});
// ❌ NoSQL Injection (MongoDB)
app.post('/login', async (req, res) => {
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
});
// Attack: {"username": {"$ne": null}, "password": {"$ne": null}}
// ✅ Secure: Sanitize input, use proper types
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session
});
3. Cross-Site Scripting (XSS)
// ❌ Reflected XSS
app.get('/search', (req, res) => {
res.send(`<h1>Results for: ${req.query.q}</h1>`);
});
// Attack: ?q=<script>alert(document.cookie)</script>
// ✅ Secure: Escape output
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
app.get('/search', (req, res) => {
res.send(`<h1>Results for: ${escapeHtml(req.query.q)}</h1>`);
});
// ✅ Better: Use templating engine with auto-escaping
app.get('/search', (req, res) => {
res.render('search', { query: req.query.q }); // Automatically escaped
});
// ✅ Content Security Policy
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
);
next();
});
4. Cross-Site Request Forgery (CSRF)
// ❌ Vulnerable: No CSRF protection
app.post('/api/transfer', authenticate, async (req, res) => {
await transferMoney(req.user.id, req.body.to, req.body.amount);
res.json({ success: true });
});
// ✅ Secure: Use CSRF tokens
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/transfer', csrfProtection, (req, res) => {
res.render('transfer', { csrfToken: req.csrfToken() });
});
app.post('/api/transfer', csrfProtection, authenticate, async (req, res) => {
await transferMoney(req.user.id, req.body.to, req.body.amount);
res.json({ success: true });
});
// ✅ Also use SameSite cookies
app.use(session({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict'
}
}));
5. Security Misconfiguration
// ❌ Exposed sensitive information
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack // Exposes internal details
});
});
// ✅ Secure: Generic error messages
app.use((err, req, res, next) => {
console.error(err); // Log internally
if (process.env.NODE_ENV === 'production') {
res.status(500).json({ error: 'Internal server error' });
} else {
res.status(500).json({ error: err.message, stack: err.stack });
}
});
// ✅ Security headers
const helmet = require('helmet');
app.use(helmet());
// ✅ Disable unnecessary features
app.disable('x-powered-by');
// ✅ Rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
Authentication & Authorization
Password Security
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// Hash password
async function hashPassword(password) {
// Validate password strength
if (password.length < 12) {
throw new Error('Password must be at least 12 characters');
}
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) ||
!/[0-9]/.test(password) || !/[^A-Za-z0-9]/.test(password)) {
throw new Error('Password must contain uppercase, lowercase, number, and special character');
}
return await bcrypt.hash(password, SALT_ROUNDS);
}
// Verify password
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// Registration
app.post('/register', async (req, res) => {
const { email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}
// Hash password
const passwordHash = await hashPassword(password);
// Create user
const user = await User.create({
email: email.toLowerCase(),
passwordHash
});
res.status(201).json({ id: user.id });
});
JWT Authentication
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET; // Use strong secret
const JWT_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
// Generate tokens
function generateTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
);
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
return { accessToken, refreshToken };
}
// Verify token middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Login
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email: email.toLowerCase() });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
// Generic error to prevent username enumeration
return res.status(401).json({ error: 'Invalid credentials' });
}
// Update last login
await user.update({ lastLoginAt: new Date() });
const tokens = generateTokens(user);
res.json(tokens);
});
// Refresh token
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const decoded = jwt.verify(refreshToken, JWT_SECRET);
if (decoded.type !== 'refresh') {
return res.status(401).json({ error: 'Invalid token type' });
}
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
const tokens = generateTokens(user);
res.json(tokens);
} catch (err) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
});
Multi-Factor Authentication (MFA)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate MFA secret
app.post('/mfa/setup', authenticate, async (req, res) => {
const secret = speakeasy.generateSecret({
name: `MyApp (${req.user.email})`
});
// Store secret temporarily
await redis.setex(`mfa:setup:${req.user.id}`, 300, secret.base32);
// Generate QR code
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
res.json({
secret: secret.base32,
qrCode
});
});
// Verify and enable MFA
app.post('/mfa/verify', authenticate, async (req, res) => {
const { token } = req.body;
const secret = await redis.get(`mfa:setup:${req.user.id}`);
if (!secret) {
return res.status(400).json({ error: 'MFA setup expired' });
}
const verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2
});
if (!verified) {
return res.status(400).json({ error: 'Invalid token' });
}
// Enable MFA for user
await User.update(req.user.id, {
mfaEnabled: true,
mfaSecret: secret
});
await redis.del(`mfa:setup:${req.user.id}`);
res.json({ success: true });
});
// Login with MFA
app.post('/login/mfa', async (req, res) => {
const { email, password, mfaToken } = req.body;
const user = await User.findOne({ email });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
if (user.mfaEnabled) {
if (!mfaToken) {
return res.status(401).json({ error: 'MFA token required' });
}
const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token: mfaToken,
window: 2
});
if (!verified) {
return res.status(401).json({ error: 'Invalid MFA token' });
}
}
const tokens = generateTokens(user);
res.json(tokens);
});
Cryptography
Encryption at Rest
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes
function encrypt(plaintext) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
encrypted,
authTag: authTag.toString('hex')
};
}
function decrypt(iv, encrypted, authTag) {
const decipher = crypto.createDecipheriv(
ALGORITHM,
KEY,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Store sensitive data
async function storeSensitiveData(userId, data) {
const { iv, encrypted, authTag } = encrypt(JSON.stringify(data));
await db.sensitiveData.create({
userId,
iv,
encrypted,
authTag
});
}
Secure Random Generation
// ✅ Cryptographically secure random
const crypto = require('crypto');
function generateSecureToken(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
function generateSecureId() {
return crypto.randomUUID();
}
// ❌ Don't use Math.random() for security
const insecureToken = Math.random().toString(36); // Predictable!
Input Validation
const { body, validationResult } = require('express-validator');
app.post('/api/users',
// Validation rules
body('email').isEmail().normalizeEmail(),
body('name').trim().isLength({ min: 2, max: 100 }).escape(),
body('age').optional().isInt({ min: 18, max: 120 }),
body('website').optional().isURL(),
async (req, res) => {
// Check validation results
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process validated data
const user = await User.create(req.body);
res.status(201).json(user);
}
);
// File upload validation
const multer = require('multer');
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024, // 5MB max
},
fileFilter: (req, file, cb) => {
// Check file type
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedMimes.includes(file.mimetype)) {
return cb(new Error('Invalid file type'));
}
// Check file extension
const ext = path.extname(file.originalname).toLowerCase();
if (!['.jpg', '.jpeg', '.png', '.gif'].includes(ext)) {
return cb(new Error('Invalid file extension'));
}
cb(null, true);
}
});
app.post('/upload', upload.single('image'), (req, res) => {
// Verify file content matches extension
// Store with random filename to prevent path traversal
const filename = `${crypto.randomUUID()}${path.extname(req.file.originalname)}`;
// Save file...
});
Security Headers
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Avoid unsafe-inline in production
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
frameguard: {
action: 'deny'
},
noSniff: true,
xssFilter: true,
}));
// Additional headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
});
Secure Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Log security events
function logSecurityEvent(event, details) {
logger.warn('Security Event', {
event,
...details,
timestamp: new Date().toISOString()
});
}
// Failed login attempts
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
logSecurityEvent('failed_login', {
email,
ip: req.ip,
userAgent: req.headers['user-agent']
});
return res.status(401).json({ error: 'Invalid credentials' });
}
// Success
logSecurityEvent('successful_login', {
userId: user.id,
ip: req.ip
});
// Generate tokens...
});
// ❌ Don't log sensitive data
logger.info('User data', user); // May contain passwordHash, tokens
// ✅ Sanitize before logging
logger.info('User data', {
id: user.id,
email: user.email,
// Omit sensitive fields
});
Best Practices
Secure Development Lifecycle
- Threat modeling
- Security requirements
- Secure coding standards
- Code review
- Security testing (SAST/DAST)
- Vulnerability scanning
- Penetration testing
- Security monitoring
Defense in Depth
- Multiple layers of security
- Assume breach mentality
- Principle of least privilege
- Input validation at every layer
- Output encoding
- Secure configuration
Security Testing
- Static Application Security Testing (SAST)
- Dynamic Application Security Testing (DAST)
- Interactive Application Security Testing (IAST)
- Software Composition Analysis (SCA)
- Penetration testing
- Bug bounty programs
Anti-Patterns to Avoid
❌ Storing passwords in plaintext: Always hash with bcrypt/argon2 ❌ Rolling your own crypto: Use established libraries ❌ Trusting user input: Validate and sanitize everything ❌ Exposing sensitive errors: Use generic error messages ❌ No rate limiting: Implement rate limiting on all endpoints ❌ Weak session management: Use secure, httpOnly cookies ❌ No logging: Log security events for monitoring ❌ Hardcoded secrets: Use environment variables
Resources
- OWASP: https://owasp.org/
- OWASP Top 10: https://owasp.org/Top10/
- CWE Top 25: https://cwe.mitre.org/top25/
- NIST Guidelines: https://www.nist.gov/cybersecurity
- Security Headers: https://securityheaders.com/
- SSL Labs: https://www.ssllabs.com/
Weekly Installs
25
Repository
personamanagmentlayer/pclFirst Seen
Jan 24, 2026
Security Audits
Installed on
opencode20
claude-code20
codex15
gemini-cli14
github-copilot12
cursor12