
Security Practices
- 52 installs
- 19 repo stars
- Updated January 20, 2026
- miles990/claude-software-skills
Helps with security tasks.
About
security-practices is a Claude Code skill for security. It helps solo builders move faster with AI-assisted development.
- security-practices
- Security
- AI-coding skill
Security Practices by the numbers
- 52 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,300 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/miles990/claude-software-skills --skill security-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 52 |
|---|---|
| repo stars | ★ 19 |
| Last updated | January 20, 2026 |
| Repository | miles990/claude-software-skills ↗ |
What it does
Helps with security tasks.
Files
Security Practices
Overview
Essential security practices for application development. Covers OWASP Top 10 and secure coding guidelines.
---
OWASP Top 10
1. Injection (SQL, NoSQL, Command)
// ❌ SQL Injection vulnerable
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Attack: email = "'; DROP TABLE users; --"
// ✅ Parameterized query
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// ✅ ORM with parameterization
const user = await prisma.user.findUnique({
where: { email }
});
// ❌ Command injection vulnerable
exec(`ping ${userInput}`);
// Attack: userInput = "google.com; rm -rf /"
// ✅ Use arrays, not string concatenation
execFile('ping', ['-c', '4', hostname]);2. Broken Authentication
// Strong password requirements
const passwordSchema = z.string()
.min(12)
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[a-z]/, 'Must contain lowercase')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
// Secure password hashing
import argon2 from 'argon2';
async function hashPassword(password: string): Promise<string> {
return argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 4
});
}
async function verifyPassword(hash: string, password: string): Promise<boolean> {
return argon2.verify(hash, password);
}
// Rate limiting login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts'
});
app.post('/login', loginLimiter, handleLogin);3. Cross-Site Scripting (XSS)
// ❌ Direct HTML insertion
element.innerHTML = userInput;
// Attack: userInput = "<script>stealCookies()</script>"
// ✅ Use textContent for text
element.textContent = userInput;
// ✅ React auto-escapes by default
function UserName({ name }: { name: string }) {
return <span>{name}</span>; // Safe
}
// ⚠️ dangerouslySetInnerHTML requires sanitization
import DOMPurify from 'dompurify';
function RichContent({ html }: { html: string }) {
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href']
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
// Content Security Policy header
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:;"
);
next();
});4. Insecure Direct Object References
// ❌ No authorization check
app.get('/api/documents/:id', async (req, res) => {
const doc = await db.documents.findById(req.params.id);
res.json(doc);
});
// Attack: User can access any document by guessing ID
// ✅ Verify ownership
app.get('/api/documents/:id', auth, async (req, res) => {
const doc = await db.documents.findById(req.params.id);
if (!doc) {
return res.status(404).json({ error: 'Not found' });
}
if (doc.ownerId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(doc);
});
// ✅ Use UUIDs instead of sequential IDs
// Harder to guess, but still check authorization!
const docId = crypto.randomUUID();5. Cross-Site Request Forgery (CSRF)
// CSRF token middleware
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) => {
// Token automatically validated
// ...
});
// In form
<form action="/submit" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<!-- form fields -->
</form>
// SameSite cookies
res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'strict' // or 'lax'
});---
Authentication
JWT Best Practices
import jwt from 'jsonwebtoken';
// Access token (short-lived)
function generateAccessToken(user: User): string {
return jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
}
// Refresh token (long-lived, stored securely)
function generateRefreshToken(user: User): string {
const token = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
// Store in database to allow revocation
db.refreshTokens.create({
userId: user.id,
token: hashToken(token),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
return token;
}
// Verify and refresh
async function refreshAccessToken(refreshToken: string) {
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!);
// Check if token is revoked
const storedToken = await db.refreshTokens.findOne({
userId: payload.sub,
token: hashToken(refreshToken)
});
if (!storedToken) {
throw new Error('Token revoked');
}
const user = await db.users.findById(payload.sub);
return generateAccessToken(user);
}OAuth 2.0 / OIDC
import { OAuth2Client } from 'google-auth-library';
const client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
'https://myapp.com/auth/google/callback'
);
// Generate auth URL
app.get('/auth/google', (req, res) => {
const url = client.generateAuthUrl({
scope: ['openid', 'email', 'profile'],
state: generateState(req.session.id) // CSRF protection
});
res.redirect(url);
});
// Handle callback
app.get('/auth/google/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (!verifyState(state, req.session.id)) {
return res.status(400).send('Invalid state');
}
// Exchange code for tokens
const { tokens } = await client.getToken(code);
// Verify ID token
const ticket = await client.verifyIdToken({
idToken: tokens.id_token,
audience: process.env.GOOGLE_CLIENT_ID
});
const payload = ticket.getPayload();
// Create or update user
const user = await upsertUser({
email: payload.email,
name: payload.name,
picture: payload.picture
});
// Create session
req.session.userId = user.id;
res.redirect('/dashboard');
});---
Authorization
Role-Based Access Control (RBAC)
// Define permissions
const PERMISSIONS = {
admin: ['read', 'write', 'delete', 'admin'],
editor: ['read', 'write'],
viewer: ['read']
} as const;
// Middleware
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
const userPermissions = PERMISSIONS[req.user.role] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Usage
app.delete('/api/posts/:id', auth, requirePermission('delete'), deletePost);Attribute-Based Access Control (ABAC)
interface Policy {
effect: 'allow' | 'deny';
resource: string;
action: string;
condition?: (context: Context) => boolean;
}
const policies: Policy[] = [
{
effect: 'allow',
resource: 'document',
action: 'read',
condition: (ctx) => ctx.resource.isPublic || ctx.user.id === ctx.resource.ownerId
},
{
effect: 'allow',
resource: 'document',
action: 'write',
condition: (ctx) => ctx.user.id === ctx.resource.ownerId
},
{
effect: 'allow',
resource: '*',
action: '*',
condition: (ctx) => ctx.user.role === 'admin'
}
];
function isAllowed(user: User, action: string, resource: Resource): boolean {
const context = { user, resource };
for (const policy of policies) {
if (
(policy.resource === '*' || policy.resource === resource.type) &&
(policy.action === '*' || policy.action === action)
) {
if (!policy.condition || policy.condition(context)) {
return policy.effect === 'allow';
}
}
}
return false; // Deny by default
}---
Secrets Management
// ❌ Never hardcode secrets
const apiKey = 'sk_live_1234567890';
// ✅ Use environment variables
const apiKey = process.env.API_KEY;
// ✅ Use secret managers
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();
async function getSecret(name: string): Promise<string> {
const [version] = await client.accessSecretVersion({
name: `projects/my-project/secrets/${name}/versions/latest`
});
return version.payload.data.toString();
}
// ✅ Rotate secrets regularly
// Store secret versions, not raw secrets
// Use short-lived tokens where possible---
Input Validation
import { z } from 'zod';
// Define strict schemas
const createUserSchema = 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()
});
// Validate at boundaries
app.post('/api/users', async (req, res) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.flatten()
});
}
// result.data is typed and validated
const user = await createUser(result.data);
res.json(user);
});
// File upload validation
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
function validateFile(file: Express.Multer.File) {
if (file.size > MAX_FILE_SIZE) {
throw new Error('File too large');
}
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new Error('Invalid file type');
}
// Also check magic bytes, not just extension
const fileType = await fileTypeFromBuffer(file.buffer);
if (!fileType || !ALLOWED_TYPES.includes(fileType.mime)) {
throw new Error('Invalid file content');
}
}---
Security Headers
import helmet from 'helmet';
app.use(helmet());
// Or configure individually
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"]
}
}));
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));---
Related Skills
- [[authentication]] - Auth patterns
- [[api-design]] - API security
- [[devops-cicd]] - Security in pipelines
# Environment Variables Security Template
# Usage: Copy to .env and fill in values
# NEVER commit .env to git - only .env.example
# ===========================================
# Application
# ===========================================
NODE_ENV=development
PORT=3000
# ===========================================
# Security Secrets (MUST be unique per environment)
# ===========================================
# JWT Secret - Generate with: openssl rand -base64 32
JWT_SECRET=your-super-secret-jwt-key-change-this
# Session Secret - Generate with: openssl rand -hex 32
SESSION_SECRET=your-session-secret-change-this
# Encryption Key - Generate with: openssl rand -base64 32
ENCRYPTION_KEY=your-encryption-key-change-this
# ===========================================
# Database
# ===========================================
# PostgreSQL
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
# MongoDB
MONGODB_URI=mongodb://localhost:27017/mydb
# Redis
REDIS_URL=redis://localhost:6379
# ===========================================
# Third-Party API Keys
# ===========================================
# Stripe
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# SendGrid / Email
SENDGRID_API_KEY=SG.xxx
# AWS
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx
AWS_REGION=us-east-1
# ===========================================
# OAuth (if applicable)
# ===========================================
# Google OAuth
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxx
# GitHub OAuth
GITHUB_CLIENT_ID=xxx
GITHUB_CLIENT_SECRET=xxx
# ===========================================
# Rate Limiting / Security
# ===========================================
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# CORS
CORS_ORIGIN=http://localhost:3000
# ===========================================
# Logging / Monitoring
# ===========================================
LOG_LEVEL=info
SENTRY_DSN=https://xxx@sentry.io/xxx
{
"_comment": "Content Security Policy Template - Adapt directives to your needs",
"_usage": "Use with helmet.js or as HTTP header value",
"strict": {
"_description": "Strict policy for high-security applications",
"default-src": ["'self'"],
"script-src": ["'self'"],
"style-src": ["'self'"],
"img-src": ["'self'", "data:"],
"font-src": ["'self'"],
"connect-src": ["'self'"],
"frame-src": ["'none'"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"],
"frame-ancestors": ["'none'"],
"upgrade-insecure-requests": true
},
"moderate": {
"_description": "Balanced policy for typical web apps",
"default-src": ["'self'"],
"script-src": ["'self'", "'unsafe-inline'"],
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
"img-src": ["'self'", "data:", "https:"],
"font-src": ["'self'", "https://fonts.gstatic.com"],
"connect-src": ["'self'", "https://api.example.com"],
"frame-src": ["https://www.youtube.com"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"]
},
"spa": {
"_description": "Policy for Single Page Applications",
"default-src": ["'self'"],
"script-src": ["'self'"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:", "blob:", "https:"],
"font-src": ["'self'", "data:"],
"connect-src": ["'self'", "wss:", "https:"],
"worker-src": ["'self'", "blob:"],
"frame-src": ["'self'"],
"object-src": ["'none'"],
"base-uri": ["'self'"]
},
"report-only": {
"_description": "Use Content-Security-Policy-Report-Only header for testing",
"_header": "Content-Security-Policy-Report-Only",
"report-uri": ["/csp-report"],
"report-to": ["csp-endpoint"]
},
"common-third-party": {
"_description": "Common third-party services to whitelist",
"google-analytics": ["https://www.google-analytics.com", "https://www.googletagmanager.com"],
"google-fonts": ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
"stripe": ["https://js.stripe.com", "https://api.stripe.com"],
"cloudflare": ["https://cdnjs.cloudflare.com"],
"jsdelivr": ["https://cdn.jsdelivr.net"],
"unpkg": ["https://unpkg.com"]
}
}
/**
* Helmet.js Security Configuration Template
* Usage: Import and use with Express
*
* npm install helmet
*/
import helmet from 'helmet';
/**
* Production-ready Helmet configuration
* Includes all recommended security headers
*/
export const helmetConfig = helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Remove unsafe-inline in production
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
connectSrc: ["'self'"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
// Cross-Origin settings
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'same-origin' },
// DNS Prefetch Control
dnsPrefetchControl: { allow: false },
// Frameguard (clickjacking protection)
frameguard: { action: 'deny' },
// Hide X-Powered-By header
hidePoweredBy: true,
// HSTS (HTTP Strict Transport Security)
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
// IE No Open
ieNoOpen: true,
// No Sniff (MIME type sniffing protection)
noSniff: true,
// Origin Agent Cluster
originAgentCluster: true,
// Permitted Cross-Domain Policies
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
// Referrer Policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// XSS Filter
xssFilter: true,
});
/**
* Development configuration (relaxed CSP)
*/
export const helmetDevConfig = helmet({
contentSecurityPolicy: false, // Disabled for development
crossOriginEmbedderPolicy: false,
});
/**
* Example Express usage:
*
* import express from 'express';
* import { helmetConfig, helmetDevConfig } from './helmet-config.js';
*
* const app = express();
*
* if (process.env.NODE_ENV === 'production') {
* app.use(helmetConfig);
* } else {
* app.use(helmetDevConfig);
* }
*/
Security Practices Templates
Ready-to-use security configuration templates for web applications.
Files
| Template | Purpose |
|---|---|
helmet-config.js | Express.js security headers (Helmet.js) |
csp-policy.json | Content Security Policy configurations |
.env.example | Environment variables template |
Usage
Helmet.js (Express Security Headers)
cp templates/helmet-config.js ./src/config/helmet-config.js
# Install
npm install helmet
# Use in Express
import { helmetConfig } from './config/helmet-config.js';
app.use(helmetConfig);Content Security Policy
Reference csp-policy.json for CSP directive examples:
- strict: High-security applications
- moderate: Typical web apps
- spa: Single Page Applications
Environment Variables
cp templates/.env.example ./.env
# Add .env to .gitignore
echo ".env" >> .gitignore
# Generate secrets
openssl rand -base64 32 # For JWT_SECRET
openssl rand -hex 32 # For SESSION_SECRETSecurity Checklist
HTTP Headers (via Helmet)
- [x] Content-Security-Policy
- [x] Strict-Transport-Security (HSTS)
- [x] X-Content-Type-Options
- [x] X-Frame-Options
- [x] X-XSS-Protection
- [x] Referrer-Policy
Secrets Management
- [ ] Never commit
.envfiles - [ ] Use different secrets per environment
- [ ] Rotate secrets regularly
- [ ] Use secret managers in production (AWS Secrets Manager, Vault)
Additional Measures
- [ ] Rate limiting
- [ ] CORS configuration
- [ ] Input validation
- [ ] SQL injection prevention
- [ ] XSS prevention
Related skills
Securityappsec