
Application Security
- 214 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Review authentication, authorization, input validation, and common OWASP risks before shipping applications to production.
About
Provides application security guidance for pre-release review: authentication and authorization, input validation, session handling, OWASP Top 10 mitigations, and secure defaults across web apps and APIs.
- OWASP coverage
- Auth hardening
- Input validation
- Session security
- Dependency risk
Application Security by the numbers
- 214 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #746 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/oakoss/agent-skills --skill application-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 214 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Review authentication, authorization, input validation, and common OWASP risks before shipping applications to production.
Files
Security
Security is built-in, not bolted-on. Every feature, endpoint, and data flow must consider security implications.
OWASP Top 10 (2025)
| # | Vulnerability | Prevention |
|---|---|---|
| 1 | Broken Access Control | Verify permissions server-side, default deny |
| 2 | Security Misconfiguration | Secure defaults, remove unused features |
| 3 | Software Supply Chain Failures | SBOM, dependency scanning, signed builds |
| 4 | Cryptographic Failures | Use TLS, hash passwords (argon2id), encrypt PII |
| 5 | Injection | Parameterized queries, input validation |
| 6 | Insecure Design | Threat modeling, security requirements |
| 7 | Authentication Failures | Strong passwords, MFA, secure session mgmt |
| 8 | Software or Data Integrity | Verify dependencies, sign releases |
| 9 | Logging and Alerting Failures | Log security events, set up alerts |
| 10 | Mishandling Exceptional Conditions | Fail securely, generic errors to clients |
Security Principles
| Principle | Rule |
|---|---|
| Defense in Depth | Multiple layers: firewall, auth, authz, encryption, audit |
| Least Privilege | Minimum permissions needed, nothing more |
| Zero Trust | Never trust, always verify. Assume breach. |
| Secure by Default | HTTPS, strict passwords, secure cookies out of the box |
| Fail Securely | Access denied on error, no internal details to users |
| Validate on Server | Client validation is UX, server validation is security |
Pre-Deployment Checklist
| Area | Requirements |
|---|---|
| Passwords | Hashed with argon2id (preferred) or bcrypt (12+ rounds) |
| Tokens | JWT with EdDSA/ES256, 15min access / 7d refresh, httpOnly cookies |
| Sessions | HttpOnly, Secure, SameSite=Strict cookies |
| Rate Limiting | Auth endpoints: 5 attempts/15min |
| Authorization | All routes check auth server-side, default deny |
| Input | Validated with schema (Zod), parameterized SQL |
| Uploads | Whitelist types, enforce size limits |
| Secrets | No secrets in code or VCS |
| Headers | CSP (with nonces), HSTS, Permissions-Policy, X-Content-Type-Options |
| CORS | Configured restrictively |
| Encryption | PII encrypted at rest (AES-256) and in transit (TLS 1.3) |
| Logging | Audit logging for security events |
| Dependencies | SBOM generated, npm audit clean, Dependabot enabled |
Threat Modeling (STRIDE)
| Threat | Category | Key Mitigations |
|---|---|---|
| Spoofing | Authentication | MFA, strong passwords, JWT with short expiry |
| Tampering | Integrity | Input validation, HTTPS/TLS, signed tokens |
| Repudiation | Accountability | Audit logging, digital signatures |
| Info Disclosure | Confidentiality | Encryption, least privilege, secret management |
| Denial of Service | Availability | Rate limiting, input validation, CDN/DDoS protection |
| Elevation of Privilege | Authorization | Authz checks on every request, ABAC, permission audits |
Risk Levels
| Level | Action |
|---|---|
| Critical | Immediate action required |
| High | Address before launch |
| Medium | Address post-launch |
| Low | Monitor, may accept risk |
Compliance Overview
| Framework | Scope | Key Requirements |
|---|---|---|
| GDPR | EU data subjects | Consent, data subject rights, breach notification (72h), DPIA |
| HIPAA | US healthcare data | PHI encryption, RBAC, audit logs, BAA with providers |
| SOC 2 | SaaS customer data | Security policies, MFA, encryption, incident response |
| PCI-DSS | Credit card data | Use payment processor (Stripe), tokenization, network segmentation |
Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
| Security as afterthought | Integrate from design phase |
| Client-side authorization | Always verify permissions server-side |
| Trusting client data (e.g., userId from body) | Get user ID from authenticated session |
| Rolling your own crypto | Use proven libraries (argon2, bcrypt, libsodium) |
| Compliance = security | Compliance is the minimum; security is ongoing |
| Verbose error responses | Generic messages to clients, details server-side |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Performing authorization checks only on the client side | Always verify permissions server-side; client checks are UX only |
| Trusting user-supplied IDs from request body (e.g., userId) | Derive user identity from the authenticated session or token |
| Rolling custom cryptography instead of using proven libraries | Use argon2id, bcrypt, or libsodium for all cryptographic operations |
| Treating compliance certification as equivalent to security | Compliance is the minimum bar; security requires ongoing review |
| Returning verbose error messages with stack traces to clients | Show generic messages to clients; log details server-side only |
Delegation
- Scan codebase for OWASP Top 10 vulnerabilities and insecure patterns: Use
Exploreagent to search for SQL injection, XSS, and hardcoded secrets - Implement authentication, authorization, and security headers end-to-end: Use
Taskagent to configure JWT, RBAC, CSP, HSTS, and rate limiting - Design a threat model and security architecture for new features: Use
Planagent to apply STRIDE methodology and map trust boundaries
For database-layer security (RLS policies, Postgres/Supabase hardening, audit trails), use thedatabase-securityskill. For AI/LLM security (prompt injection defense, agentic zero-trust, MCP tool hardening), use thesecure-aiskill.
References
- Threat Modeling — STRIDE methodology, risk assessment process, trust boundaries
- Authentication and Authorization — JWT, session-based, OAuth, RBAC, ABAC, IDOR protection
- API Security — OWASP API Security Top 10, object-level authorization, rate limiting, SSRF prevention, security testing
- Input Validation — SQL injection, XSS, command injection, path traversal, Zod validation, file upload security
- Data Protection — Password hashing (argon2id/bcrypt), AES-256-GCM encryption, secrets management
- Secure Configuration — Security headers, CORS, Express hardening, rate limiting
- Supply Chain Security — SBOM generation, dependency scanning, CI/CD hardening, artifact signing
- Monitoring and Compliance — Audit logging, error handling, GDPR/HIPAA/SOC2/PCI-DSS, troubleshooting
API Security
OWASP API Security Top 10 (2023)
Separate from the general OWASP Top 10, this list targets API-specific vulnerabilities.
| # | Risk | Prevention |
|---|---|---|
| API1 | Broken Object Level Authorization | Verify user owns the resource on every request |
| API2 | Broken Authentication | Strong auth, token rotation, MFA, brute-force protection |
| API3 | Broken Object Property Level Authorization | Allowlist response fields, validate writable properties |
| API4 | Unrestricted Resource Consumption | Rate limiting, pagination limits, payload size caps |
| API5 | Broken Function Level Authorization | Verify role/permissions for each endpoint, not just auth |
| API6 | Unrestricted Access to Sensitive Business Flows | Protect critical workflows (checkout, transfer) with CAPTCHA |
| API7 | Server Side Request Forgery (SSRF) | Validate and allowlist outbound URLs, block internal networks |
| API8 | Security Misconfiguration | Secure defaults, disable debug, restrict CORS, security headers |
| API9 | Improper Inventory Management | Document all endpoints, deprecate old versions, audit exposure |
| API10 | Unsafe Consumption of APIs | Validate data from third-party APIs, enforce timeouts |
Object Level Authorization (API1)
The most common API vulnerability. Always verify the requesting user owns or has access to the specific resource.
app.get('/api/documents/:id', auth, async (req, res) => {
const doc = await db.document.findFirst({
where: {
id: req.params.id,
userId: req.user.id,
},
});
if (!doc) return res.status(404).json({ error: 'Not found' });
res.json(doc);
});Never rely on obscure IDs (UUIDs) as access control. Always enforce ownership checks server-side.
Object Property Level Authorization (API3)
Control which properties users can read and write. Prevent mass assignment and excessive data exposure.
const UpdateProfileSchema = z.object({
name: z.string().min(1).max(100),
bio: z.string().max(500).optional(),
});
app.patch('/api/profile', auth, async (req, res) => {
const validated = UpdateProfileSchema.parse(req.body);
await db.user.update({
where: { id: req.user.id },
data: validated,
});
res.json({ success: true });
});Allowlist fields on both input (writable) and output (readable). Never spread raw request bodies into database operations.
Rate Limiting Strategies
Per-User Tiered Limits
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
const apiLimiter = rateLimit({
store: new RedisStore({ client: redis, prefix: 'rl:api:' }),
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.user?.id || req.ip,
});
const authLimiter = rateLimit({
store: new RedisStore({ client: redis, prefix: 'rl:auth:' }),
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
});
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);Rate Limit Headers
Always return standard rate limit headers so clients can self-throttle:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1700000000
Retry-After: 30SSRF Prevention (API7)
import { URL } from 'url';
import ipaddr from 'ipaddr.js';
const ALLOWED_PROTOCOLS = ['https:'];
const BLOCKED_RANGES = ['private', 'loopback', 'linkLocal'];
function validateExternalUrl(input: string): URL {
const url = new URL(input);
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
throw new Error('Only HTTPS URLs allowed');
}
const addr = ipaddr.parse(url.hostname);
if (BLOCKED_RANGES.some((range) => addr.range() === range)) {
throw new Error('Internal network access denied');
}
return url;
}Never allow user-supplied URLs to reach internal services, metadata endpoints (169.254.169.254), or localhost.
API Versioning Security
- Deprecate old API versions with sunset headers
- Monitor traffic to deprecated versions for exploitation attempts
- Never leave undocumented endpoints exposed
- Use
API-Versionor URL path versioning consistently
Security Headers for APIs
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Cache-Control', 'no-store');
res.setHeader('X-Frame-Options', 'DENY');
res.removeHeader('X-Powered-By');
next();
});APIs should set Cache-Control: no-store on sensitive responses to prevent caching of authentication tokens or user data.
Security Testing Patterns
describe('API Security', () => {
test('requires authentication', async () => {
const res = await fetch('/api/protected');
expect(res.status).toBe(401);
});
test('prevents IDOR', async () => {
const res = await fetch(`/api/documents/${otherUserDocId}`, {
headers: { Authorization: `Bearer ${userToken}` },
});
expect(res.status).toBe(404);
});
test('enforces rate limits', async () => {
const requests = Array.from({ length: 6 }, () =>
fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email: 'test@test.com', password: 'wrong' }),
}),
);
const responses = await Promise.all(requests);
expect(responses.some((r) => r.status === 429)).toBe(true);
});
test('rejects invalid input', async () => {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ email: 'not-an-email' }),
});
expect(res.status).toBe(400);
});
});Authentication and Authorization
JWT (JSON Web Tokens)
When: Stateless APIs, mobile apps, microservices. Use asymmetric algorithms (EdDSA preferred, ES256 widely supported, RS256 for legacy compatibility). Never use HS256 in distributed systems. Short expiry (15min access, 7d refresh). Store in httpOnly cookies.
Algorithm priority: EdDSA (best security + performance) > ES256 (modern + widely supported) > RS256 (maximum compatibility). Always whitelist allowed algorithms in verification to prevent algorithm confusion attacks.
import { SignJWT, jwtVerify, importPKCS8, importSPKI } from 'jose';
const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, 'ES256');
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, 'ES256');
export async function createToken(userId: string) {
return await new SignJWT({ userId })
.setProtectedHeader({ alg: 'ES256' })
.setIssuedAt()
.setExpirationTime('15m')
.sign(privateKey);
}
export async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ['ES256'],
});
return payload;
}Token Refresh Pattern
async function refreshAccessToken(refreshToken: string) {
const { payload } = await jwtVerify(refreshToken, publicKey, {
algorithms: ['ES256'],
});
const newAccessToken = await createToken(payload.userId as string);
return { accessToken: newAccessToken };
}Rotate refresh tokens on each use (one-time use). Store refresh token family to detect reuse attacks and revoke the entire family if a used token is presented again.
Session-Based Auth
Server stores session ID in encrypted cookie (HttpOnly, Secure, SameSite=Strict). Regenerate session ID after login to prevent session fixation.
OAuth 2.0 / OIDC
Use established libraries (NextAuth.js / Auth.js, Auth0). Validate state parameter. Use PKCE (Proof Key for Code Exchange) for all clients, not just mobile -- PKCE is now recommended for all OAuth 2.0 flows per RFC 9126.
RBAC (Role-Based Access Control)
enum Role {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
function requireRole(allowedRoles: Role[]) {
return (req, res, next) => {
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.delete('/api/users/:id', requireRole([Role.ADMIN]), deleteUser);ABAC (Attribute-Based)
More granular, e.g., user can edit resource only if they created it.
IDOR Protection
const doc = await db.document.findFirst({
where: {
id: req.params.id,
userId: req.user.id,
},
});
if (!doc) return res.status(404).json({ error: 'Not found' });Data Protection
Password Hashing
Algorithm priority (per OWASP): argon2id > scrypt > bcrypt > PBKDF2 (FIPS only).
Argon2id (Preferred)
Memory-hard algorithm resistant to GPU/ASIC attacks. Use argon2id variant for combined protection against side-channel and time-memory tradeoff attacks.
import argon2 from 'argon2';
// Hash on signup — OWASP minimum: memoryCost 19456 (19 MiB), timeCost 2
const hashedPassword = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 47104,
timeCost: 1,
parallelism: 1,
});
await db.user.create({ email, password: hashedPassword });
// Verify on login
const isValid = await argon2.verify(user.password, password);
if (!isValid) throw new Error('Invalid credentials');bcrypt (Legacy / Fallback)
Acceptable when argon2id is unavailable. Use cost factor 12 or higher, targeting 250-500ms hash time.
import bcrypt from 'bcrypt';
const hashedPassword = await bcrypt.hash(password, 12);
await db.user.create({ email, password: hashedPassword });
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) throw new Error('Invalid credentials');Always use async hash/compare to avoid blocking the event loop. Benchmark on production hardware and adjust cost parameters so hashing takes 250ms-1s.
Encryption at Rest (AES-256-GCM)
import crypto from 'crypto';
const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
function encrypt(text: string) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
const encrypted = Buffer.concat([
cipher.update(text, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
encryptedData: encrypted.toString('hex'),
authTag: authTag.toString('hex'),
};
}
function decrypt(encrypted: {
iv: string;
encryptedData: string;
authTag: string;
}) {
const decipher = crypto.createDecipheriv(
algorithm,
key,
Buffer.from(encrypted.iv, 'hex'),
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
return (
decipher.update(encrypted.encryptedData, 'hex', 'utf8') +
decipher.final('utf8')
);
}Secrets Management
- Use AWS Secrets Manager, HashiCorp Vault, or Doppler in production
- Environment variables for config (never in code)
- Rotate secrets regularly (90 days)
- Use IAM roles over long-lived credentials
Input Validation
Prevent SQL Injection
// BAD: string interpolation
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
// GOOD: parameterized queries
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [userInput]);
// GOOD: ORM handles it
const user = await prisma.user.findUnique({ where: { email: userInput } });Prevent XSS
React escapes by default. When rendering raw HTML is unavoidable, sanitize with DOMPurify:
import DOMPurify from 'isomorphic-dompurify';
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'a'],
ALLOWED_ATTR: ['href'],
});Set Content Security Policy headers to limit damage from any bypass. See the Secure Configuration reference.
Prevent Command Injection
Never pass user input to shell commands. Use argument arrays instead of string interpolation:
import { execFile } from 'child_process';
// BAD: shell injection via string
exec(`convert ${userFilename} output.png`);
// GOOD: argument array, no shell interpretation
execFile('convert', [userFilename, 'output.png']);If shell execution is unavoidable, validate input against a strict allowlist:
const ALLOWED_FORMATS = ['png', 'jpg', 'webp'];
if (!ALLOWED_FORMATS.includes(format)) {
throw new Error('Invalid format');
}Prevent Path Traversal
import path from 'path';
function safePath(baseDir: string, userPath: string): string {
const resolved = path.resolve(baseDir, userPath);
if (!resolved.startsWith(path.resolve(baseDir))) {
throw new Error('Path traversal detected');
}
return resolved;
}
// Usage
const filePath = safePath('/uploads', req.params.filename);Never use user input directly in fs.readFile, fs.writeFile, or similar calls without resolving and validating the resulting path.
Schema Validation with Zod
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email().max(255),
password: z
.string()
.min(8)
.max(100)
.regex(/[A-Z]/, 'Needs uppercase')
.regex(/[a-z]/, 'Needs lowercase')
.regex(/[0-9]/, 'Needs number'),
age: z.number().int().min(13).max(120),
});
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}Validate all user input at system boundaries. Client-side validation is UX; server-side validation is security.
File Upload Security
const upload = multer({
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('Invalid file type'));
}
cb(null, true);
},
});
const filename = crypto.randomUUID() + path.extname(file.originalname);Validate both MIME type and file extension. Generate random filenames to prevent directory traversal. Store uploads outside the web root or use a storage service (S3, R2).
Output Encoding
Encode output based on context to prevent injection:
| Context | Encoding |
|---|---|
| HTML body | HTML entity encoding (React default) |
| HTML attr | Attribute encoding, quote wrapping |
| JavaScript | JSON.stringify or template literal escaping |
| URL param | encodeURIComponent |
| SQL | Parameterized queries (never string concat) |
| Shell | Argument arrays (never string interpolation) |
Monitoring and Compliance
Audit Logging
async function auditLog(event: {
userId?: string;
action: string;
resource: string;
ip: string;
userAgent: string;
success: boolean;
metadata?: Record<string, unknown>;
}) {
await db.auditLog.create({ data: { ...event, timestamp: new Date() } });
}Error Handling
// BAD: exposes stack trace
res.status(500).json({ error: err.stack });
// GOOD: generic message, log full error server-side
logger.error(err);
res
.status(500)
.json({ error: 'Internal server error', requestId: generateRequestId() });Tools
- Sentry — Error tracking, security alerts
- OWASP ZAP — Automated web vulnerability scanner
- Snyk / npm audit — Dependency vulnerability scanning
- Datadog / CloudWatch — APM, anomaly detection
GDPR (EU Data Subjects)
// Right to Access (DSAR)
app.get('/api/user/data-export', authMiddleware, async (req, res) => {
const userId = req.user.id;
const userData = {
profile: await db.users.findById(userId),
projects: await db.projects.findByUser(userId),
activity: await db.activityLog.findByUser(userId),
};
res.setHeader('Content-Disposition', 'attachment; filename=my-data.json');
res.json(userData);
});
// Right to Erasure
app.delete('/api/user/account', authMiddleware, async (req, res) => {
const userId = req.user.id;
await db.users.update(userId, {
email: `deleted-${userId}@example.com`,
name: 'Deleted User',
deleted_at: new Date(),
});
await db.sessions.deleteByUser(userId);
res.json({ message: 'Account deleted successfully' });
});Troubleshooting
JWT Token Issues
- "Token expired" — Access token TTL too short or clock skew. Use 15min access + 7d refresh. Allow 30s clock skew.
- "Invalid signature" — Secret mismatch between services. Ensure all services share the same secret/key pair.
- "Algorithm mismatch" — Mixing symmetric/asymmetric algorithms. Standardize on ES256 or EdDSA for production. Always whitelist allowed algorithms in verification.
CORS Errors
- "No Access-Control-Allow-Origin" — Origin not in allowed list. Check exact match including protocol and port.
- Preflight fails — Ensure OPTIONS requests are handled and return correct headers.
- Credentials not sent — Set
credentials: truein CORS config andcredentials: 'include'in fetch.
Rate Limiting False Positives
- Users behind shared IP (corporate NAT) hit limits. Use user ID + IP for keying when authenticated.
- Load balancer forwarding — ensure
X-Forwarded-Foris trusted and parsed correctly.
CSP Violations
- Inline scripts blocked — Use nonces (
'nonce-xxx') instead of'unsafe-inline'. Generate a unique nonce per response. - Third-party scripts blocked — Add specific domains to
script-srcdirective. Avoid wildcards. - Clickjacking — Use
frame-ancestors 'none'in CSP instead of the legacyX-Frame-Optionsheader. - Start with
Content-Security-Policy-Report-Onlyto test before enforcing. Set up a reporting endpoint to collect violations.
Password Hashing Performance
- argon2id with OWASP minimum parameters (19 MiB, t=2) targets ~250-500ms. Preferred for new projects due to GPU/ASIC resistance.
- bcrypt with 12 rounds also targets ~250ms. Acceptable for existing systems.
- Always use async hash functions to avoid blocking the event loop.
- Benchmark on production hardware and adjust parameters so hashing takes 250ms-1s.
Encryption Key Rotation
- Implement envelope encryption: encrypt data with DEK, encrypt DEK with KEK.
- Store key version alongside encrypted data to support decryption during rotation.
- AWS KMS and Google Cloud KMS handle rotation automatically.
Security Testing
Manual tests before deployment:
1. SQL injection: ' OR '1'='1 2. XSS: <script>alert('XSS')</script> 3. CSRF: Submit form from different origin 4. Path traversal: ../../etc/passwd 5. Auth bypass: Access protected routes without token 6. IDOR: Change resource IDs in URLs to access other users' data
Secure Configuration
Security Headers
import crypto from 'crypto';
const nonce = crypto.randomBytes(16).toString('base64');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'; frame-ancestors 'none'`,
);
response.headers.set(
'Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload',
);
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=()',
);Header notes:
- X-XSS-Protection — Deprecated. Remove or set to
0. Use CSP instead. - X-Frame-Options — Legacy. Use CSP
frame-ancestors 'none'instead (more granular control). - CSP nonces — Preferred over
'unsafe-inline'for script/style sources. Generate a unique nonce per response. - Permissions-Policy — Restricts browser features (camera, microphone, geolocation). Replaces the deprecated Feature-Policy header.
- HSTS preload — Add
preloaddirective and submit to the HSTS preload list for maximum protection.
CORS Configuration
app.use(
cors({
origin:
process.env.NODE_ENV === 'production'
? 'https://yourdomain.com'
: 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}),
);Express Hardening
import helmet from 'helmet';
app.use(helmet());
app.use(express.json({ limit: '10mb' }));Rate Limiting
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests from this IP',
});
app.use(limiter);
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
});
app.post('/api/auth/login', authLimiter, loginHandler);Supply Chain Security
Software Supply Chain Failures is ranked #3 in the OWASP Top 10 (2025), expanding beyond vulnerable components to cover the entire ecosystem of dependencies, build systems, and distribution.
Dependency Management
Lockfile Integrity
Always commit lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock). Use --frozen-lockfile in CI to prevent unexpected dependency changes.
# CI installation — fails if lockfile is out of sync
pnpm install --frozen-lockfile
# Audit for known vulnerabilities
pnpm audit --audit-level=highAutomated Scanning
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10
groups:
production:
dependency-type: production
development:
dependency-type: developmentSupplement Dependabot with runtime scanning tools (Snyk, Socket, OWASP Dependency-Check) that detect malicious packages, not just known CVEs.
Reducing Attack Surface
# Find unused dependencies
npx depcheck
# Remove unnecessary packages
pnpm remove unused-packageMinimize dependency count. Prefer well-maintained packages with active security response teams. Check download counts, last publish date, and maintainer count before adopting new dependencies.
SBOM (Software Bill of Materials)
Generate machine-readable SBOMs in CycloneDX or SPDX format as part of every release. SBOMs enable rapid incident response when a vulnerability is discovered in a transitive dependency.
# Generate CycloneDX SBOM from package-lock.json
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Generate SPDX SBOM
npx spdx-sbom-generator -o sbom-spdx.jsonAutomate SBOM generation in CI/CD so every build produces an updated inventory. Store SBOMs alongside release artifacts for auditing and compliance.
CI/CD Pipeline Hardening
Access Control
# GitHub Actions — principle of least privilege
permissions:
contents: read
packages: write- Enforce MFA for all accounts with write access to repositories and registries
- Use short-lived tokens (OIDC) instead of long-lived secrets where possible
- Require code review and branch protection on main/release branches
- Use ephemeral CI runners (containers/VMs destroyed after each job)
Build Reproducibility
# Pin action versions by SHA, not mutable tags
# BAD: uses: actions/checkout@v6 (mutable tag)
# GOOD: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11Pin all CI action versions to full commit SHAs to prevent supply chain attacks via tag hijacking. Use npm ci or pnpm install --frozen-lockfile to ensure deterministic installs.
Secret Scanning
# GitHub Actions — scan for leaked secrets
- name: Secret scan
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verifiedScan repositories for accidentally committed secrets (API keys, tokens, passwords). Block pushes containing secrets using pre-commit hooks or server-side push rules.
Artifact Signing and Provenance
Sign release artifacts and container images to verify they have not been tampered with. Use Sigstore/cosign for container image signing or npm provenance for package publishing.
# Publish npm package with provenance (links package to source repo and build)
npm publish --provenance
# Sign container images with cosign
cosign sign --key cosign.key your-registry.com/your-image:tagProvenance attestations let consumers verify that an artifact was built from the expected source code by a trusted build system.
Monitoring and Response
Vulnerability Alerting
- Enable GitHub security advisories and Dependabot alerts
- Configure Snyk or Socket for real-time dependency risk monitoring
- Subscribe to security mailing lists for critical dependencies
- Monitor the OSV (Open Source Vulnerabilities) database
Incident Response Drill
Periodically simulate a supply chain incident:
1. Identify which services use the affected package (via SBOM) 2. Determine exposure window (when vulnerable version was deployed) 3. Roll back or patch affected deployments 4. Rotate any potentially compromised credentials 5. Communicate status to stakeholders
Key Metrics
| Metric | Target |
|---|---|
| Services with generated SBOMs | 100% |
| Mean time to remediate critical dep CVEs | Under 72h |
| Repos with branch protection | 100% |
| CI pipelines with dependency scanning | 100% |
| Artifacts with signed provenance | All releases |
Threat Modeling
Apply STRIDE at each trust boundary (User-App, App-API, API-DB, Internal-External).
STRIDE Categories
S - Spoofing (Authentication)
- Threat: Attacker impersonates a user or system
- Mitigations: MFA, strong password policies, JWT with short expiration, secure session management
T - Tampering (Integrity)
- Threat: Attacker modifies data or code
- Mitigations: Input validation, HTTPS/TLS everywhere, signed tokens, integrity checks (hashing)
R - Repudiation (Accountability)
- Threat: User denies performing an action
- Mitigations: Comprehensive audit logging, digital signatures, immutable log storage
I - Information Disclosure (Confidentiality)
- Threat: Sensitive data exposed to unauthorized parties
- Mitigations: Encryption at rest and in transit, least privilege, secret management (Vault, AWS Secrets Manager), RBAC
D - Denial of Service (Availability)
- Threat: System becomes unavailable
- Mitigations: Rate limiting, input validation (reject massive payloads), auto-scaling, CDN/DDoS protection
E - Elevation of Privilege (Authorization)
- Threat: User gains unauthorized higher privileges
- Mitigations: Authorization checks on every request, least privilege, ABAC, regular permission audits
Threat Modeling Process
1. Identify Assets — User data, business data, credentials, infrastructure 2. Identify Trust Boundaries — User-App, App-API, API-DB, Internal-External, Admin-Production 3. Apply STRIDE at each boundary 4. Assess Risk — Likelihood (High/Medium/Low) x Impact (Critical/High/Medium/Low) 5. Define Mitigations — Strategy, effort, residual risk, owner, timeline