
Software Security Appsec
- 207 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
software-security-appsec is an agent skill providing production-grade application security patterns aligned to OWASP Top 10:2025 and NIST SSDF for auth, input validation, and cryptography.
About
software-security-appsec is a Claude Code skill providing production-grade AppSec patterns aligned to OWASP Top 10:2025 and NIST SSDF. It covers authentication decisions (passkeys/WebAuthn, OAuth 2.1 + PKCE, JWT), input validation, parameterized queries, AES-256-GCM encryption, TLS 1.3, RBAC/ABAC, rate limiting, and secure SDLC gates like threat modeling and dependency scanning. Developers use it when implementing auth, handling untrusted input, working with cryptography, or reviewing security posture. Bundled references cover XSS, API security, cryptography standards, and incident response.
- OWASP Top 10:2025 and NIST SSDF alignment
- Auth decision matrix (passkeys, OAuth 2.1, JWT)
- Reference library on XSS, crypto, and incident response
Software Security Appsec by the numbers
- 207 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #761 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
software-security-appsec capabilities & compatibility
free, no API key
- Capabilities
- security audit · authentication · input validation · cryptography · threat modeling
- Use cases
- security audit · api development
- Pricing
- Free
What software-security-appsec says it does
AppSec patterns aligned with OWASP Top 10:2025 and NIST SSDF.
Production-grade security patterns for building secure applications in Jan 2026.
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill software-security-appsecAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 207 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
How do I implement auth, input validation, and cryptography that meets OWASP Top 10:2025 and NIST SSDF baselines?
security-audit
Who is it for?
Developers implementing or reviewing application security controls (auth, crypto, input handling) against modern standards.
Skip if: General backend development without a security focus, cloud/infra security, or smart-contract auditing as the primary focus.
When should I use this skill?
Implementing authentication or authorization, handling injection-prone input, working with cryptography, or conducting security reviews and threat modeling.
What you get
Security controls and reviews aligned to OWASP Top 10:2025 with concrete auth, crypto, and input-validation patterns.
- Security controls following OWASP/NIST patterns
- Security-posture review findings
By the numbers
- OWASP Top 10:2025 checklist
- 10-row security-task quick-reference table
Files
Software Security & AppSec — Quick Reference
Production-grade security patterns for building secure applications in Jan 2026. Covers OWASP Top 10:2025 (stable) https://owasp.org/Top10/2025/ plus OWASP API Security Top 10 (2023) https://owasp.org/API-Security/ and secure SDLC baselines (NIST SSDF) https://csrc.nist.gov/publications/detail/sp/800-218/final.
---
When to Use This Skill
Activate this skill when:
- Implementing authentication or authorization systems
- Handling user input that could lead to injection attacks (SQL, XSS, command injection)
- Designing secure APIs or web applications
- Working with cryptographic operations or sensitive data storage
- Conducting security reviews, threat modeling, or vulnerability assessments
- Responding to security incidents or compliance audit requirements
- Building systems that must comply with OWASP, NIST, PCI DSS, GDPR, HIPAA, or SOC 2
- Integrating third-party dependencies (supply chain security review)
- Implementing zero trust architecture or modern cloud-native security patterns
- Establishing or improving secure SDLC gates (threat modeling, SAST/DAST, dependency scanning)
When NOT to Use This Skill
- General backend development without security focus → use software-backend
- Infrastructure/cloud security (IAM, network security, container hardening) → use ops-devops-platform
- Smart contract auditing as primary focus → use software-crypto-web3
- ML model security (adversarial attacks, data poisoning) → use ai-mlops
- Compliance-only questions without implementation → consult compliance team directly
---
Quick Reference Table
| Security Task | Tool/Pattern | Implementation | When to Use |
|---|---|---|---|
| Primary Auth | Passkeys/WebAuthn | navigator.credentials.create() | New apps (2026+), phishing-resistant, broad platform support |
| Password Storage | bcrypt/Argon2 | bcrypt.hash(password, 12) | Legacy auth fallback (never store plaintext) |
| Input Validation | Allowlist regex | /^[a-zA-Z0-9_]{3,20}$/ | All user input (SQL, XSS, command injection prevention) |
| SQL Queries | Parameterized queries | db.execute(query, [userId]) | All database operations (prevent SQL injection) |
| API Authentication | OAuth 2.1 + PKCE | oauth.authorize({ code_challenge }) | Third-party auth, API access (deprecates implicit flow) |
| Token Auth | JWT (short-lived) | jwt.sign(payload, secret, { expiresIn: '15m' }) | Stateless APIs (always validate, 15-30 min expiry) |
| Data Encryption | AES-256-GCM | crypto.createCipheriv('aes-256-gcm') | Sensitive data at rest (PII, financial, health) |
| HTTPS/TLS | TLS 1.3 | Force HTTPS redirects | All production traffic (data in transit) |
| Access Control | RBAC/ABAC | requireRole('admin', 'moderator') | Resource authorization (APIs, admin panels) |
| Rate Limiting | express-rate-limit | limiter({ windowMs: 15min, max: 100 }) | Public APIs, auth endpoints (DoS prevention) |
| Security Requirements | OWASP ASVS | Choose L1/L2/L3 | Security requirements baseline + test scope |
Authentication Decision Matrix (Jan 2026)
| Method | Use Case | Token Lifetime | Security Level | Notes |
|---|---|---|---|---|
| Passkeys/WebAuthn | Primary auth (2026+) | N/A (cryptographic) | Highest | Phishing-resistant, broad platform support |
| OAuth 2.1 + PKCE | Third-party auth | 5-15 min access | High | Replaces implicit flow, mandatory PKCE |
| Session cookies | Traditional web apps | 30 min - 4 hrs | Medium-High | HttpOnly, Secure, SameSite=Strict |
| JWT stateless | APIs, microservices | 15-30 min | Medium | Always validate signature, short expiry |
| API keys | Machine-to-machine | Long-lived | Low-Medium | Rotate regularly, scope permissions |
Jurisdiction notes (verify): Authentication assurance requirements vary by country, industry, and buyer. Prefer passkeys/FIDO2; treat SMS OTP as recovery-only/low assurance unless you can justify it.
OWASP Top 10:2025 Quick Checklist
| # | Risk | Key Controls | Test |
|---|---|---|---|
| A01 | Broken Access Control | RBAC/ABAC, deny by default, CORS allowlist | BOLA, BFLA, privilege escalation |
| A02 | Security Misconfiguration | Harden defaults, disable unused features, error handling | Default creds, stack traces, headers |
| A03 | Supply Chain Failures (NEW) | SBOM, dependency scanning, SLSA, code signing | Outdated deps, typosquatting, compromised packages |
| A04 | Cryptographic Failures | TLS 1.3, AES-256-GCM, key rotation, no MD5/SHA1 | Weak ciphers, exposed secrets, cert validation |
| A05 | Injection | Parameterized queries, input validation, output encoding | SQLi, XSS, command injection, LDAP injection |
| A06 | Insecure Design | Threat modeling, secure design patterns, abuse cases | Design flaws, missing controls, trust boundaries |
| A07 | Authentication Failures | MFA/passkeys, rate limiting, secure password storage | Credential stuffing, brute force, session fixation |
| A08 | Integrity Failures | Code signing, CI/CD pipeline security, SRI | Unsigned updates, pipeline poisoning, CDN tampering |
| A09 | Logging Failures | Structured JSON, SIEM integration, correlation IDs | Missing logs, PII in logs, no alerting |
| A10 | Exceptional Conditions (NEW) | Fail-safe defaults, complete error recovery, input validation | Error handling gaps, fail-open, resource exhaustion |
Decision Tree: Security Implementation
Security requirement: [Feature Type]
├─ User Authentication?
│ ├─ Session-based? → Cookie sessions + CSRF tokens
│ ├─ Token-based? → JWT with refresh tokens (references/authentication-authorization.md)
│ └─ Third-party? → OAuth2/OIDC integration
│
├─ User Input?
│ ├─ Database query? → Parameterized queries (NEVER string concatenation)
│ ├─ HTML output? → DOMPurify sanitization + CSP headers
│ ├─ File upload? → Content validation, size limits, virus scanning
│ └─ API parameters? → Allowlist validation (references/input-validation.md)
│
├─ Sensitive Data?
│ ├─ Passwords? → bcrypt/Argon2 (cost factor 12+)
│ ├─ PII/financial? → AES-256-GCM encryption + key rotation
│ ├─ API keys/tokens? → Environment variables + secrets manager
│ └─ In transit? → TLS 1.3 only
│
├─ Access Control?
│ ├─ Simple roles? → RBAC (assets/web-application/template-authorization.md)
│ ├─ Complex rules? → ABAC with policy engine
│ └─ Relationship-based? → ReBAC (owner, collaborator, viewer)
│
└─ API Security?
├─ Public API? → Rate limiting + API keys
├─ CORS needed? → Strict origin allowlist (never *)
└─ Headers? → Helmet.js (CSP, HSTS, X-Frame-Options)---
Security ROI & Business Value (Jan 2026)
Security investment justification and compliance-driven revenue. Full framework: references/security-business-value.md
Quick Breach Cost Reference
Indicative figures (source: IBM Cost of a Data Breach 2024; refresh for current year): https://www.ibm.com/reports/data-breach
| Metric | Global Avg | US Avg | Impact |
|---|---|---|---|
| Avg breach cost | $4.88M | $9.36M | Budget justification baseline |
| Cost per record | $165 | $194 | Data classification priority |
| Detection time | 204 days | 191 days | SIEM/monitoring ROI |
| DevSecOps adoption | -$1.68M | -34% | Shift-left justification |
| IR team | -$2.26M | -46% | Highest ROI control |
Compliance → Enterprise Sales
| Certification | Deals Unlocked | Sales Impact |
|---|---|---|
| SOC 2 Type II | $100K+ enterprise | Typically reduces security questionnaire friction |
| ISO 27001 | $250K+ EU enterprise | Preferred vendor status |
| HIPAA | Healthcare vertical | Market access |
| FedRAMP | $1M+ government | US gov market entry |
ROI Formula (Quick Reference)
Security ROI = (Risk Reduction - Investment) / Investment × 100
Risk Reduction = Breach Probability × Avg Cost × Control Effectiveness
Example: 15% × $4.88M × 46% = $337K/year risk reduction---
Incident Response Patterns (Jan 2026)
Security Incident Playbook
| Phase | Actions |
|---|---|
| Detect | Alert fires, user report, automated scan |
| Contain | Isolate affected systems, revoke compromised credentials |
| Investigate | Collect logs, determine scope, identify root cause |
| Remediate | Patch vulnerability, rotate secrets, update defenses |
| Recover | Restore services, verify fixes, update monitoring |
| Learn | Post-mortem, update playbooks, share lessons |
Security Logging Requirements
| What to Log | Format | Retention |
|---|---|---|
| Authentication events | JSON with correlation ID | 90 days minimum |
| Authorization failures | JSON with user context | 90 days minimum |
| Data access (sensitive) | JSON with resource ID | 1 year minimum |
| Security scan results | SARIF format | 1 year minimum |
Do:
- Include correlation IDs across services
- Log to SIEM (Splunk, Datadog, ELK)
- Mask PII in logs
Avoid:
- Logging passwords, tokens, or keys
- Unstructured log formats
- Missing timestamps or context
Common Security Mistakes
| FAIL Bad Practice | PASS Correct Approach | Risk |
|---|---|---|
query = "SELECT * FROM users WHERE id=" + userId | db.execute("SELECT * FROM users WHERE id=?", [userId]) | SQL injection |
| Storing passwords in plaintext or MD5 | bcrypt.hash(password, 12) or Argon2 | Credential theft |
res.send(userInput) without encoding | res.send(DOMPurify.sanitize(userInput)) | XSS |
| Hardcoded API keys in source code | Environment variables + secrets manager | Secret exposure |
Access-Control-Allow-Origin: * | Explicit origin allowlist | CORS bypass |
| JWT with no expiration | expiresIn: '15m' + refresh tokens | Token hijacking |
| Generic error messages to logs | Structured JSON with correlation IDs | Debugging blind spots |
| SMS OTP as primary factor | Passkeys/WebAuthn or TOTP (keep SMS for recovery-only) | Credential phishing |
---
Optional: AI/Automation Extensions
Note: Security considerations for AI systems. Skip if not building AI features.
LLM Security Patterns
| Threat | Mitigation |
|---|---|
| Prompt injection | Input validation, output filtering, sandboxed execution |
| Data exfiltration | Output scanning, PII detection |
| Model theft | API rate limiting, watermarking |
| Jailbreaking | Constitutional AI, guardrails |
AI-Assisted Security Tools
| Tool | Use Case |
|---|---|
| Semgrep | Static analysis with AI rules |
| Snyk Code | AI-powered vulnerability detection |
| GitHub CodeQL | Semantic code analysis |
---
.NET/EF Core Crypto Integration Security
For C#/.NET crypto/fintech services using Entity Framework Core, see:
- references/dotnet-efcore-crypto-security.md — Security rules and C# patterns
Key rules summary:
- No secrets in code — use configuration/environment variables
- No sensitive data in logs (tokens, keys, PII)
- Use
decimalfor financial values, neverdouble/float - EF Core or parameterized queries only — no dynamic SQL
- Generic error messages to users, detailed logging server-side
Navigation
Core Resources (Updated 2024-2026)
Security Business Value & ROI
- references/security-business-value.md — Breach cost modeling, security ROI formulas, compliance → enterprise sales, investment justification templates
2025 Updates & Modern Architecture
- references/supply-chain-security.md — Dependency, build, and artifact integrity (SLSA, provenance, signing)
- references/zero-trust-architecture.md — NIST SP 800-207, service identity, policy-based access
- references/owasp-top-10.md — OWASP Top 10:2025 (final) guide + 2021→2025 diffs
- references/advanced-xss-techniques.md — 2024-2025 XSS: mutation XSS, polyglots, SVG attacks, context-aware encoding
API Security, Incident Response & Threat Modeling
- references/api-security-patterns.md — OWASP API Security Top 10, BOLA/BFLA, rate limiting, API keys, GraphQL/gRPC security
- references/incident-response-playbook.md — IR team roles, severity triage, containment by incident type, evidence handling, communication templates, postmortem
- references/threat-modeling-guide.md — STRIDE, PASTA, data flow diagrams, attack trees, risk scoring (CVSS/DREAD), lightweight agile threat modeling
Foundation Security Patterns
- references/secure-design-principles.md — Defense in depth, least privilege, secure defaults
- references/authentication-authorization.md — AuthN/AuthZ flows, OAuth 2.1, JWT best practices, RBAC/ABAC
- references/input-validation.md — Allowlist validation, SQL injection, XSS, CSRF prevention, file upload security
- references/cryptography-standards.md — AES-256-GCM, Argon2, TLS 1.3, key management
- references/common-vulnerabilities.md — Path traversal, command injection, deserialization, SSRF
External References
- data/sources.json — 70+ curated security resources (OWASP 2025, supply chain, zero trust, API security, compliance)
- Shared checklists: ../software-clean-code-standard/assets/checklists/secure-code-review-checklist.md, ../software-clean-code-standard/assets/checklists/backend-api-review-checklist.md
Templates by Domain
Web Application Security
- assets/web-application/template-authentication.md — Secure authentication flows (JWT, OAuth2, sessions, MFA)
- assets/web-application/template-authorization.md — RBAC/ABAC/ReBAC policy patterns
API Security
- assets/api/template-secure-api.md — Secure API gateway, rate limiting, CORS, security headers
Cloud-Native Security
- assets/cloud-native/crypto-security.md — Cryptography usage, key management, HSM integration
Blockchain & Web3 Security
- references/smart-contract-security-auditing.md — NEW: Smart contract auditing, vulnerability patterns, formal verification, Solidity security
Related Skills
Security Ecosystem
- ../software-backend/SKILL.md — API implementation patterns and error handling
- ../software-architecture-design/SKILL.md — Secure system decomposition and dependency design
- ../ops-devops-platform/SKILL.md — DevSecOps pipelines, secrets management, infrastructure hardening
- ../software-crypto-web3/SKILL.md — Smart contract security, blockchain vulnerabilities, DeFi patterns
- ../qa-testing-strategy/SKILL.md — Security testing, SAST/DAST integration, penetration testing
AI/LLM Security
- ../ai-llm/SKILL.md — LLM security patterns including prompt injection prevention
- ../ai-mlops/SKILL.md — ML model security, adversarial attacks, privacy-preserving ML
Quality & Resilience
- ../qa-resilience/SKILL.md — Resilience, safeguards, failure handling, chaos engineering
- ../qa-refactoring/SKILL.md — Security-focused refactoring patterns
---
Trend Awareness Protocol
IMPORTANT: When users ask recommendation questions about application security, you MUST use WebSearch to check current trends before answering. If WebSearch is unavailable, use data/sources.json + web browsing and state what you verified vs assumed.
Trigger Conditions
- "What's the best approach for [authentication/authorization]?"
- "What should I use for [secrets/encryption/API security]?"
- "What's the latest in application security?"
- "Current best practices for [OWASP/zero trust/supply chain]?"
- "Is [security approach] still recommended in 2026?"
- "What are the latest security vulnerabilities?"
- "Best auth solution for [use case]?"
Required Searches
1. Search: "application security best practices 2026" 2. Search: "OWASP Top 10 2025 2026" 3. Search: "[authentication/authorization] trends 2026" 4. Search: "supply chain security 2026"
What to Report
After searching, provide:
- Current landscape: What security approaches are standard NOW
- Emerging threats: New vulnerabilities or attack vectors
- Deprecated/declining: Approaches that are no longer secure
- Recommendation: Based on fresh data and current advisories
Example Topics (verify with fresh search)
- OWASP Top 10 updates
- Passkeys and passwordless authentication
- AI security concerns (prompt injection, model poisoning)
- Supply chain security (SBOMs, dependency scanning)
- Zero trust architecture implementation
- API security (BOLA, broken auth)
---
Pre-Implementation Security Gate
Before building any feature that involves storage, uploads, or user-generated content:
1. Threat model first: Identify what an attacker could do with this feature (file upload → malware, storage → data exfiltration, user content → XSS). 2. Check OWASP mapping: Map the feature to relevant OWASP Top 10 categories above. 3. Define constraints before coding: Set file type allowlist, size limits, storage isolation, and access controls before writing the first line. 4. Review existing security patterns: Check if the project already has upload/storage security utilities to reuse.
Building storage/upload features without upfront security constraints leads to retroactive hardening that is more expensive and error-prone.
Operational Playbooks
- references/operational-playbook.md — Core security principles, OWASP summaries, authentication patterns, and detailed code examples
Fact-Checking
- Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.
- Prefer primary sources; report source links and dates for volatile information.
- If web access is unavailable, state the limitation and mark guidance as unverified.
Secure API Implementation Template
Copy-paste ready implementation for building secure REST APIs with comprehensive security controls.
---
Complete Secure API Setup
Dependencies
npm install express helmet cors express-rate-limit express-validator winston morganEnvironment Configuration
# .env
NODE_ENV=production
PORT=3000
API_VERSION=v1
# Security
JWT_SECRET=your-secret-key-min-32-characters
ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# Logging
LOG_LEVEL=infoSecurity Headers
// config/security.js
const helmet = require('helmet');
const securityHeaders = helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'", 'data:'],
connectSrc: ["'self'"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
},
// HTTP Strict Transport Security
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
// X-Frame-Options
frameguard: {
action: 'deny'
},
// X-Content-Type-Options
noSniff: true,
// X-XSS-Protection
xssFilter: true,
// Referrer-Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
},
// X-Permitted-Cross-Domain-Policies
permittedCrossDomainPolicies: {
permittedPolicies: 'none'
},
// Hide X-Powered-By
hidePoweredBy: true
});
module.exports = securityHeaders;CORS Configuration
// config/cors.js
const cors = require('cors');
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',')
: [];
const corsOptions = {
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, Postman, etc.)
if (!origin) {
return callback(null, true);
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-CSRF-Token'
],
exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
maxAge: 86400 // 24 hours
};
module.exports = cors(corsOptions);Rate Limiting
// config/rateLimiting.js
const rateLimit = require('express-rate-limit');
// General API rate limiter
const apiLimiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
message: {
error: 'Too many requests from this IP, please try again later'
},
standardHeaders: true,
legacyHeaders: false,
// Skip successful requests from the count
skipSuccessfulRequests: false,
// Key generator (use IP + user ID if authenticated)
keyGenerator: (req) => {
return req.user ? `${req.ip}-${req.user.userId}` : req.ip;
}
});
// Stricter rate limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
skipSuccessfulRequests: true,
message: {
error: 'Too many authentication attempts, please try again later'
}
});
// Very strict rate limiter for password reset
const passwordResetLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
message: {
error: 'Too many password reset requests, please try again later'
}
});
// Strict limiter for sensitive operations
const sensitiveLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: {
error: 'Too many requests for this operation'
}
});
module.exports = {
apiLimiter,
authLimiter,
passwordResetLimiter,
sensitiveLimiter
};Input Validation
// middleware/validation.js
const { body, param, query, validationResult } = require('express-validator');
// Validation error handler
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array().map(err => ({
field: err.param,
message: err.msg,
value: err.value
}))
});
}
next();
};
// Common validators
const validators = {
email: body('email')
.trim()
.isEmail().withMessage('Invalid email format')
.normalizeEmail()
.isLength({ max: 254 }).withMessage('Email too long'),
password: body('password')
.isLength({ min: 12 }).withMessage('Password must be at least 12 characters')
.matches(/[A-Z]/).withMessage('Password must contain uppercase letter')
.matches(/[a-z]/).withMessage('Password must contain lowercase letter')
.matches(/\d/).withMessage('Password must contain number')
.matches(/[!@#$%^&*(),.?":{}|<>]/).withMessage('Password must contain special character'),
id: param('id')
.isMongoId().withMessage('Invalid ID format'),
uuid: param('id')
.isUUID().withMessage('Invalid UUID format'),
pagination: [
query('page')
.optional()
.isInt({ min: 1 }).withMessage('Page must be positive integer')
.toInt(),
query('limit')
.optional()
.isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100')
.toInt()
],
string: (field, minLength = 1, maxLength = 255) =>
body(field)
.trim()
.isLength({ min: minLength, max: maxLength })
.withMessage(`${field} must be between ${minLength} and ${maxLength} characters`)
.escape(),
enum: (field, allowedValues) =>
body(field)
.isIn(allowedValues)
.withMessage(`${field} must be one of: ${allowedValues.join(', ')}`)
};
module.exports = {
handleValidationErrors,
validators
};Logging
// config/logger.js
const winston = require('winston');
const morgan = require('morgan');
// Winston logger configuration
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'api' },
transports: [
// Write all logs to console
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// Write all logs to combined.log
new winston.transports.File({ filename: 'logs/combined.log' }),
// Write error logs to error.log
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
// Write security events to security.log
new winston.transports.File({
filename: 'logs/security.log',
level: 'warn'
})
]
});
// Security logger (separate from general logging)
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { type: 'security' },
transports: [
new winston.transports.File({ filename: 'logs/security.log' })
]
});
// Morgan HTTP request logger
const httpLogger = morgan('combined', {
stream: {
write: (message) => logger.http(message.trim())
}
});
// Sanitize logs (remove sensitive data)
const sanitizeForLogging = (data) => {
const sensitiveFields = [
'password',
'passwordHash',
'token',
'refreshToken',
'accessToken',
'apiKey',
'ssn',
'creditCard',
'cvv'
];
const sanitized = { ...data };
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
};
module.exports = {
logger,
securityLogger,
httpLogger,
sanitizeForLogging
};Error Handling
// middleware/errorHandler.js
const { logger } = require('../config/logger');
// Custom error classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class AuthenticationError extends AppError {
constructor(message) {
super(message, 401);
}
}
class AuthorizationError extends AppError {
constructor(message) {
super(message, 403);
}
}
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
}
}
// Global error handler
const errorHandler = (err, req, res, next) => {
let { statusCode = 500, message } = err;
// Log error
logger.error('Error occurred', {
error: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
userId: req.user?.userId
});
// Mongoose validation error
if (err.name === 'ValidationError') {
statusCode = 400;
message = Object.values(err.errors).map(e => e.message).join(', ');
}
// Mongoose duplicate key error
if (err.code === 11000) {
statusCode = 400;
message = 'Duplicate field value';
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
statusCode = 401;
message = 'Invalid token';
}
if (err.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Token expired';
}
// Send error response
if (process.env.NODE_ENV === 'production') {
// Production: Generic error message
res.status(statusCode).json({
error: err.isOperational ? message : 'Internal server error',
...(statusCode === 400 && err.details ? { details: err.details } : {})
});
} else {
// Development: Detailed error
res.status(statusCode).json({
error: message,
stack: err.stack,
...(err.details ? { details: err.details } : {})
});
}
};
// 404 handler
const notFoundHandler = (req, res) => {
res.status(404).json({
error: 'Endpoint not found',
path: req.originalUrl
});
};
// Async error wrapper
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
module.exports = {
AppError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
errorHandler,
notFoundHandler,
asyncHandler
};Main Application
// app.js
const express = require('express');
const securityHeaders = require('./config/security');
const corsConfig = require('./config/cors');
const { apiLimiter } = require('./config/rateLimiting');
const { httpLogger } = require('./config/logger');
const { errorHandler, notFoundHandler } = require('./middleware/errorHandler');
const app = express();
// Trust proxy (if behind reverse proxy)
app.set('trust proxy', 1);
// Security middleware (apply first)
app.use(securityHeaders);
app.use(corsConfig);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// HTTP request logging
app.use(httpLogger);
// Rate limiting
app.use('/api/', apiLimiter);
// Health check (no rate limit, no auth)
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// API versioning
const API_VERSION = process.env.API_VERSION || 'v1';
// Routes
app.use(`/api/${API_VERSION}/auth`, require('./routes/auth'));
app.use(`/api/${API_VERSION}/users`, require('./routes/users'));
app.use(`/api/${API_VERSION}/posts`, require('./routes/posts'));
// 404 handler
app.use(notFoundHandler);
// Global error handler (must be last)
app.use(errorHandler);
module.exports = app;Secure Route Example
// routes/posts.js
const express = require('express');
const authenticate = require('../middleware/authenticate');
const { requirePermission } = require('../middleware/authorize');
const { validators, handleValidationErrors } = require('../middleware/validation');
const { asyncHandler, NotFoundError } = require('../middleware/errorHandler');
const { sensitiveLimiter } = require('../config/rateLimiting');
const { PERMISSIONS } = require('../config/permissions');
const Post = require('../models/Post');
const router = express.Router();
// List posts (public, with pagination)
router.get('/',
validators.pagination,
handleValidationErrors,
asyncHandler(async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
Post.find({ published: true })
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 }),
Post.countDocuments({ published: true })
]);
res.json({
data: posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
})
);
// Get single post (public)
router.get('/:id',
validators.id,
handleValidationErrors,
asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post || !post.published) {
throw new NotFoundError('Post not found');
}
res.json(post);
})
);
// Create post (authenticated, requires permission)
router.post('/',
authenticate,
requirePermission(PERMISSIONS.POSTS_WRITE),
[
validators.string('title', 1, 200),
validators.string('content', 1, 10000),
validators.enum('status', ['draft', 'published'])
],
handleValidationErrors,
asyncHandler(async (req, res) => {
const post = await Post.create({
...req.body,
authorId: req.user.userId
});
res.status(201).json(post);
})
);
// Update post (authenticated, requires ownership or permission)
router.put('/:id',
authenticate,
validators.id,
[
validators.string('title', 1, 200).optional(),
validators.string('content', 1, 10000).optional(),
validators.enum('status', ['draft', 'published']).optional()
],
handleValidationErrors,
asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) {
throw new NotFoundError('Post not found');
}
// Check ownership or permission
if (post.authorId !== req.user.userId && req.user.role !== 'moderator') {
throw new AuthorizationError('Not authorized to update this post');
}
Object.assign(post, req.body);
await post.save();
res.json(post);
})
);
// Delete post (authenticated, requires permission, rate limited)
router.delete('/:id',
sensitiveLimiter,
authenticate,
requirePermission(PERMISSIONS.POSTS_DELETE),
validators.id,
handleValidationErrors,
asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) {
throw new NotFoundError('Post not found');
}
await post.remove();
res.json({ message: 'Post deleted successfully' });
})
);
module.exports = router;Server Entry Point
// server.js
require('dotenv').config();
const app = require('./app');
const { logger } = require('./config/logger');
const mongoose = require('mongoose');
const PORT = process.env.PORT || 3000;
// Database connection
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => {
logger.info('Connected to MongoDB');
})
.catch((error) => {
logger.error('MongoDB connection error:', error);
process.exit(1);
});
// Start server
const server = app.listen(PORT, () => {
logger.info(`Server running on port ${PORT} in ${process.env.NODE_ENV} mode`);
});
// Graceful shutdown
const gracefulShutdown = () => {
logger.info('Received shutdown signal, closing server gracefully');
server.close(() => {
logger.info('Server closed');
mongoose.connection.close(false, () => {
logger.info('MongoDB connection closed');
process.exit(0);
});
});
// Force shutdown after 10 seconds
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection:', { reason, promise });
gracefulShutdown();
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
gracefulShutdown();
});---
API Documentation Template
/**
* @api {get} /api/v1/posts List posts
* @apiName ListPosts
* @apiGroup Posts
* @apiVersion 1.0.0
*
* @apiParam {Number} [page=1] Page number
* @apiParam {Number} [limit=20] Items per page (max 100)
*
* @apiSuccess {Object[]} data Array of posts
* @apiSuccess {Object} pagination Pagination info
*
* @apiSuccessExample Success Response:
* HTTP/1.1 200 OK
* {
* "data": [...],
* "pagination": {
* "page": 1,
* "limit": 20,
* "total": 100,
* "totalPages": 5
* }
* }
*
* @apiError (400) ValidationError Invalid parameters
* @apiError (429) RateLimitExceeded Too many requests
*
* @apiRateLimit 100 requests per 15 minutes
*/---
Security Testing
// tests/security.test.js
const request = require('supertest');
const app = require('../app');
describe('Security', () => {
test('Should set security headers', async () => {
const res = await request(app).get('/health');
expect(res.headers['x-content-type-options']).toBe('nosniff');
expect(res.headers['x-frame-options']).toBe('DENY');
expect(res.headers['x-xss-protection']).toBe('1; mode=block');
expect(res.headers['strict-transport-security']).toBeTruthy();
});
test('Should enforce rate limiting', async () => {
const endpoint = '/api/v1/auth/login';
// Make 6 requests (limit is 5)
for (let i = 0; i < 6; i++) {
const res = await request(app)
.post(endpoint)
.send({ email: 'test@example.com', password: 'password' });
if (i < 5) {
expect(res.status).not.toBe(429);
} else {
expect(res.status).toBe(429);
}
}
});
test('Should reject XSS attempts', async () => {
const res = await request(app)
.post('/api/v1/posts')
.send({
title: '<script>alert("XSS")</script>',
content: 'Normal content'
});
expect(res.body.title).not.toContain('<script>');
});
test('Should validate input', async () => {
const res = await request(app)
.post('/api/v1/posts')
.send({
title: 'a'.repeat(201), // Exceeds max length
content: 'content'
});
expect(res.status).toBe(400);
});
});---
Security Checklist
- [ ] HTTPS enforced (TLS 1.3)
- [ ] Security headers configured (Helmet)
- [ ] CORS properly configured
- [ ] Rate limiting on all endpoints
- [ ] Input validation on all inputs
- [ ] Output encoding/sanitization
- [ ] Authentication required where needed
- [ ] Authorization checked on every request
- [ ] Comprehensive error handling
- [ ] Security logging for sensitive operations
- [ ] Secrets in environment variables
- [ ] API versioning implemented
- [ ] Request size limits
- [ ] Graceful error messages (no stack traces in production)
- [ ] 404 handler for unknown endpoints
- [ ] Health check endpoint
- [ ] Graceful shutdown handling
Smart Contract Security Audit Checklist
Comprehensive security audit checklist for blockchain smart contracts focusing on vulnerability detection and exploit prevention.
---
OWASP Smart Contract Top 10
SC-1: Reentrancy
Description: External contract calls that allow attackers to recursively call back into the calling contract.
Detection:
// VULNERABLE PATTERN
function withdraw() public {
uint amount = balances[msg.sender];
(bool success,) = msg.sender.call{value: amount}(""); // External call
balances[msg.sender] = 0; // State change AFTER call
}Exploit Scenario: 1. Attacker calls withdraw() 2. During the .call(), attacker's fallback is triggered 3. Fallback calls withdraw() again before balance is zeroed 4. Attacker drains contract
Mitigation:
- Use Checks-Effects-Interactions pattern
- Apply ReentrancyGuard modifier
- Update state before external calls
Test:
function testReentrancyAttack() public {
// Deploy attack contract
// Verify attack fails with ReentrancyGuard
}---
SC-2: Access Control
Description: Missing or improper access control on privileged functions.
Vulnerable Patterns:
// BAD: No access control
function mint(address to, uint amount) public {
_mint(to, amount);
}
// BAD: Using tx.origin
function withdraw() public {
require(tx.origin == owner); // Phishing vulnerable
}Mitigation:
// GOOD: Proper access control
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Secure is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
function mint(address to, uint amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}---
SC-3: Arithmetic Issues
Description: Integer overflow/underflow vulnerabilities.
Detection:
- Solidity <0.8: Check for SafeMath usage
- Solidity ≥0.8: Verify appropriate use of
unchecked - Look for unchecked blocks with user input
Vulnerable:
// Solidity 0.7.x
uint256 balance = 100;
balance = balance - 200; // Underflows to MAX_UINT256Secure:
// Solidity 0.8+
uint256 balance = 100;
balance = balance - 200; // Reverts automatically---
SC-4: Unchecked Return Values
Description: Not checking return values of external calls.
Vulnerable:
// BAD: Ignores return value
token.transfer(recipient, amount);Secure:
// GOOD: Checks return value
require(token.transfer(recipient, amount), "Transfer failed");
// GOOD: Using SafeERC20
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(recipient, amount);---
SC-5: Denial of Service
Description: Attackers cause contract functions to become unusable.
Patterns:
// BAD: VULNERABLE: Unbounded loop
function distributeRewards() public {
for (uint i = 0; i < users.length; i++) { // Can exceed gas limit
users[i].transfer(reward);
}
}
// BAD: VULNERABLE: Revert on failure
function withdraw() public {
for (uint i = 0; i < recipients.length; i++) {
recipients[i].transfer(amounts[i]); // One failure = all fail
}
}Mitigation:
// GOOD: Pull over push
mapping(address => uint) public pendingRewards;
function claimReward() public {
uint reward = pendingRewards[msg.sender];
pendingRewards[msg.sender] = 0;
payable(msg.sender).transfer(reward);
}---
SC-6: Front-Running / MEV
Description: Attackers observe pending transactions and submit their own with higher gas to profit.
Vulnerable Scenarios:
- DEX trades without slippage protection
- Dutch auctions
- Commit-reveal schemes without proper implementation
Mitigation:
// Commit-Reveal Pattern
mapping(address => bytes32) public commitments;
function commit(bytes32 commitment) public {
commitments[msg.sender] = commitment;
}
function reveal(uint256 value, bytes32 salt) public {
require(keccak256(abi.encodePacked(value, salt)) == commitments[msg.sender]);
// Process value
}
// Slippage Protection
function swap(uint amountIn, uint minAmountOut) public {
uint amountOut = calculateSwapAmount(amountIn);
require(amountOut >= minAmountOut, "Slippage too high");
}---
SC-7: Time Manipulation
Description: Reliance on block.timestamp for critical logic.
Risk: Miners can manipulate timestamp by ~15 seconds.
Vulnerable:
// BAD: Using timestamp for short periods
function claim() public {
require(block.timestamp > lastClaim + 1 minutes);
}Secure:
// GOOD: Use block.number for short periods
function claim() public {
require(block.number > lastClaimBlock + 4); // ~1 minute (15s blocks)
}---
SC-8: Delegatecall to Untrusted Callee
Description: Using delegatecall with user-controlled addresses.
Vulnerable:
// BAD: CRITICAL VULNERABILITY
function execute(address target, bytes memory data) public {
target.delegatecall(data); // Attacker can modify storage
}Secure:
// GOOD: Whitelist approved implementations
mapping(address => bool) public approvedImplementations;
function execute(address target, bytes memory data) public {
require(approvedImplementations[target], "Untrusted target");
target.delegatecall(data);
}---
SC-9: Insufficient Gas Griefing
Description: Relying on gas stipends that can be insufficient.
Vulnerable:
// BAD: Using .transfer() or .send()
payable(recipient).transfer(amount); // Only 2300 gasSecure:
// GOOD: Using .call() with error handling
(bool success,) = payable(recipient).call{value: amount}("");
require(success, "Transfer failed");---
SC-10: Flash Loan Attacks
Description: Exploiting protocols using borrowed funds within single transaction.
Attack Vectors:
- Price oracle manipulation
- Governance manipulation
- Liquidity pool manipulation
Mitigation:
// GOOD: Check balances at transaction end
uint256 balanceBefore = token.balanceOf(address(this));
// ... operations ...
require(token.balanceOf(address(this)) >= balanceBefore, "Flash loan attack detected");
// GOOD: Use TWAP oracles
function getPrice() public view returns (uint256) {
return oracle.getTWAP(3600); // 1-hour TWAP
}---
DeFi-Specific Vulnerabilities
Price Oracle Manipulation
// BAD: VULNERABLE: Using spot price
uint price = token0.balanceOf(pair) / token1.balanceOf(pair);
// GOOD: SECURE: Using Chainlink price feed
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
function getLatestPrice() public view returns (int) {
(
uint80 roundId,
int price,
,
uint updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(price > 0, "Invalid price");
require(updatedAt > 0, "Round not complete");
require(answeredInRound >= roundId, "Stale price");
require(block.timestamp - updatedAt < PRICE_VALIDITY, "Price too old");
return price;
}Rounding Errors
// BAD: VULNERABLE: Rounds in favor of user
uint fee = (amount * 3) / 1000; // 0.3% fee rounds down
// GOOD: SECURE: Rounds in favor of protocol
uint fee = (amount * 3 + 999) / 1000; // Rounds up---
Upgradeable Contract Vulnerabilities
Storage Collision
// BAD: VULNERABLE: Reordering variables
contract V1 {
uint256 public a;
uint256 public b;
}
contract V2 is V1 {
uint256 public c; // Adds before existing
uint256 public a; // COLLISION!
uint256 public b;
}
// GOOD: SECURE: Append new variables
contract V2 is V1 {
uint256 public c; // Appends after existing
}Uninitialized Implementation
// BAD: VULNERABLE: No constructor protection
contract Implementation {
function initialize() public {
owner = msg.sender;
}
}
// GOOD: SECURE: Disable initializers in constructor
contract Implementation {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize() public initializer {
owner = msg.sender;
}
}---
Solana-Specific Vulnerabilities
Missing Signer Checks
// BAD: VULNERABLE
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
// No signer validation
}
// GOOD: SECURE
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub from: Signer<'info>, // Enforces signer
#[account(mut)]
pub to: AccountInfo<'info>,
}Account Validation
// BAD: VULNERABLE: No ownership check
let token_account = &ctx.accounts.token_account;
// GOOD: SECURE: Validate ownership
require_keys_eq!(
ctx.accounts.token_account.owner,
token::ID,
ErrorCode::InvalidTokenAccount
);---
Automated Security Tools
Static Analysis
# Slither - Static analysis framework
slither contracts/
# Mythril - Symbolic execution
myth analyze contracts/Token.sol
# Manticore - Dynamic symbolic execution
manticore contracts/Token.solFuzzing
# Echidna - Property-based fuzzer
echidna-test contracts/ --contract Token --config echidna.yaml
# Foundry invariant testing
forge test --match-contract InvariantFormal Verification
# Certora Prover
certoraRun contracts/Token.sol --verify Token:specs/Token.spec---
Audit Process
1. Preparation:
- Clone repository
- Install dependencies
- Compile contracts
- Run existing tests
2. Automated Analysis:
- Run Slither
- Run Mythril/Manticore
- Run fuzzing tools
- Check Solhint/Ethlint
3. Manual Review:
- Read contracts line-by-line
- Check against this checklist
- Identify attack vectors
- Document findings
4. Testing:
- Write exploit POCs
- Verify mitigations
- Test edge cases
- Measure gas costs
5. Reporting:
- Classify by severity
- Provide exploit scenarios
- Recommend mitigations
- Suggest improvements
---
Severity Classification
Critical (9.0-10.0):
- Direct loss of funds
- Unauthorized access to funds
- Protocol manipulation
High (7.0-8.9):
- Potential loss of funds under specific conditions
- Smart contract freezing
- Unauthorized state changes
Medium (4.0-6.9):
- State inconsistency
- Failure to deliver promised functionality
- Suboptimal design patterns
Low (1.0-3.9):
- Code quality issues
- Gas inefficiencies
- Best practice violations
Informational (0.0):
- Code style
- Documentation
- Suggestions
---
Common Attack Patterns to Test
1. Reentrancy: Recursive calls 2. Access Control: Unauthorized function calls 3. Front-Running: Transaction ordering manipulation 4. Overflow/Underflow: Arithmetic boundaries 5. Flash Loans: Single-transaction exploits 6. Oracle Manipulation: Price feed attacks 7. DoS: Gas limit attacks, reverting recipients 8. Phishing: tx.origin usage 9. Delegate Call: Storage manipulation 10. Rounding: Precision loss exploitation
---
Resources
Mobile Application Security Template
Use this template for implementing security best practices in iOS and Android applications.
Mobile Security Checklist (OWASP MASVS)
Data Storage
- [ ] Sensitive data encrypted at rest
- [ ] No sensitive data in logs
- [ ] Secure key storage (Keychain/Keystore)
- [ ] Auto-lock after inactivity
- [ ] Clear clipboard after timeout
Authentication
- [ ] Multi-factor authentication support
- [ ] Biometric authentication (Face ID/Touch ID)
- [ ] Certificate pinning for API calls
- [ ] Secure session management
- [ ] OAuth 2.0 with PKCE
Network Security
- [ ] TLS 1.3 for all connections
- [ ] Certificate pinning implemented
- [ ] No sensitive data in URLs
- [ ] API request/response validation
- [ ] WebView security hardening
Code Security
- [ ] Code obfuscation enabled
- [ ] No hardcoded secrets
- [ ] Jailbreak/root detection
- [ ] Anti-debugging measures
- [ ] Secure random number generation
Platform Security
- [ ] Latest SDK version
- [ ] Secure intent handling (Android)
- [ ] Secure deep link validation
- [ ] App Transport Security (iOS)
- [ ] Android backup disabled for sensitive data
iOS Security Implementation
Secure Data Storage (Swift)
// SecureStorage.swift - Using iOS Keychain
import Foundation
import Security
class SecureStorage {
// MARK: - Save to Keychain
static func save(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else {
return false
}
// Delete existing item first
delete(key: key)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly // Device-specific
]
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
// MARK: - Retrieve from Keychain
static func load(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value
}
// MARK: - Delete from Keychain
static func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
// MARK: - Save Sensitive Data with Biometric Protection
static func saveBiometric(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else {
return false
}
delete(key: key)
// Create access control for biometric authentication
guard let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet, // Requires Face ID/Touch ID
nil
) else {
return false
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessControl as String: access
]
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
}
// Usage
SecureStorage.save(key: "authToken", value: "sensitive_token_here")
let token = SecureStorage.load(key: "authToken")Certificate Pinning (Swift + Alamofire)
// NetworkManager.swift
import Foundation
import Alamofire
class NetworkManager {
static let shared = NetworkManager()
private let session: Session
private init() {
// Certificate pinning configuration
let evaluators: [String: ServerTrustEvaluating] = [
"api.yourdomain.com": PinnedCertificatesTrustEvaluator(
certificates: [
// Load certificate from bundle
NetworkManager.certificate(filename: "api_certificate")!
],
acceptSelfSignedCertificates: false,
performDefaultValidation: true,
validateHost: true
)
]
let serverTrustManager = ServerTrustManager(evaluators: evaluators)
self.session = Session(
serverTrustManager: serverTrustManager
)
}
// MARK: - Helper: Load Certificate
private static func certificate(filename: String) -> SecCertificate? {
guard let path = Bundle.main.path(forResource: filename, ofType: "cer"),
let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
return SecCertificateCreateWithData(nil, data as CFData)
}
// MARK: - API Request
func request<T: Decodable>(
_ endpoint: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
completion: @escaping (Result<T, Error>) -> Void
) {
session.request(
"https://api.yourdomain.com\(endpoint)",
method: method,
parameters: parameters,
encoding: JSONEncoding.default
)
.validate()
.responseDecodable(of: T.self) { response in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
completion(.failure(error))
}
}
}
}Biometric Authentication (Swift)
// BiometricAuth.swift
import LocalAuthentication
class BiometricAuth {
enum BiometricType {
case faceID
case touchID
case none
}
enum AuthError: Error {
case biometricNotAvailable
case biometricNotEnrolled
case authenticationFailed
case userCancel
case userFallback
}
// MARK: - Check Biometric Availability
static func biometricType() -> BiometricType {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
return .none
}
switch context.biometryType {
case .faceID:
return .faceID
case .touchID:
return .touchID
default:
return .none
}
}
// MARK: - Authenticate
static func authenticate(
reason: String = "Authenticate to access your account",
completion: @escaping (Result<Void, AuthError>) -> Void
) {
let context = LAContext()
var error: NSError?
// Check if biometric authentication is available
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
if let error = error {
switch error.code {
case LAError.biometryNotEnrolled.rawValue:
completion(.failure(.biometricNotEnrolled))
default:
completion(.failure(.biometricNotAvailable))
}
}
return
}
// Perform authentication
context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
) { success, error in
DispatchQueue.main.async {
if success {
completion(.success(()))
} else if let error = error as? LAError {
switch error.code {
case .userCancel:
completion(.failure(.userCancel))
case .userFallback:
completion(.failure(.userFallback))
default:
completion(.failure(.authenticationFailed))
}
}
}
}
}
}
// Usage
BiometricAuth.authenticate { result in
switch result {
case .success:
print("Authentication successful")
// Proceed with sensitive operation
case .failure(let error):
print("Authentication failed: \(error)")
}
}Jailbreak Detection (Swift)
// JailbreakDetection.swift
import UIKit
class JailbreakDetection {
// MARK: - Check if Device is Jailbroken
static func isJailbroken() -> Bool {
#if targetEnvironment(simulator)
return false // Always allow simulator
#else
return checkSuspiciousFiles() || checkSuspiciousApps() || checkWriteAccess()
#endif
}
// MARK: - Check for Suspicious Files
private static func checkSuspiciousFiles() -> Bool {
let suspiciousFiles = [
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/",
"/private/var/lib/cydia",
"/private/var/mobile/Library/SBSettings/Themes",
"/private/var/tmp/cydia.log",
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist"
]
for path in suspiciousFiles {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
return false
}
// MARK: - Check for Suspicious Apps
private static func checkSuspiciousApps() -> Bool {
let suspiciousApps = [
"cydia://",
"sileo://",
"zbra://",
"undecimus://",
"checkra1n://"
]
for urlScheme in suspiciousApps {
if let url = URL(string: urlScheme),
UIApplication.shared.canOpenURL(url) {
return true
}
}
return false
}
// MARK: - Check Write Access to Restricted Areas
private static func checkWriteAccess() -> Bool {
let testPath = "/private/jailbreak_test.txt"
do {
try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
try FileManager.default.removeItem(atPath: testPath)
return true // Should not be able to write here
} catch {
return false // Cannot write, device is safe
}
}
// MARK: - Handle Jailbroken Device
static func handleJailbrokenDevice() {
guard isJailbroken() else { return }
// Option 1: Show warning and continue
let alert = UIAlertController(
title: "Security Warning",
message: "Your device appears to be jailbroken. Some features may not work correctly.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
// Option 2: Exit app (more aggressive)
// exit(0)
// Option 3: Disable sensitive features
// UserDefaults.standard.set(true, forKey: "isJailbroken")
}
}
// Usage in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if JailbreakDetection.isJailbroken() {
JailbreakDetection.handleJailbrokenDevice()
}
return true
}Android Security Implementation
Secure Data Storage (Kotlin)
// SecureStorage.kt - Using Android Keystore
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
class SecureStorage(context: Context) {
// Use EncryptedSharedPreferences for simple key-value storage
private val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// MARK: - Save/Load with EncryptedSharedPreferences
fun saveString(key: String, value: String) {
sharedPreferences.edit().putString(key, value).apply()
}
fun loadString(key: String): String? {
return sharedPreferences.getString(key, null)
}
fun delete(key: String) {
sharedPreferences.edit().remove(key).apply()
}
// MARK: - Advanced: Android Keystore Encryption
companion object {
private const val KEYSTORE_ALIAS = "SecureStorageKey"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val AES_MODE = "AES/GCM/NoPadding"
fun encrypt(data: ByteArray): Pair<ByteArray, ByteArray> {
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
val encryptedData = cipher.doFinal(data)
val iv = cipher.iv
return Pair(encryptedData, iv)
}
fun decrypt(encryptedData: ByteArray, iv: ByteArray): ByteArray {
val cipher = Cipher.getInstance(AES_MODE)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
return cipher.doFinal(encryptedData)
}
private fun getOrCreateKey(): SecretKey {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
keyStore.load(null)
if (!keyStore.containsAlias(KEYSTORE_ALIAS)) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE
)
val keySpec = KeyGenParameterSpec.Builder(
KEYSTORE_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false) // Set to true for biometric
.build()
keyGenerator.init(keySpec)
keyGenerator.generateKey()
}
return keyStore.getKey(KEYSTORE_ALIAS, null) as SecretKey
}
}
}
// Usage
val secureStorage = SecureStorage(context)
secureStorage.saveString("authToken", "sensitive_token_here")
val token = secureStorage.loadString("authToken")Certificate Pinning (Kotlin + OkHttp)
// NetworkClient.kt
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object NetworkClient {
private const val BASE_URL = "https://api.yourdomain.com/"
// Certificate pinning configuration
private val certificatePinner = CertificatePinner.Builder()
.add("api.yourdomain.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // Your certificate hash
.add("api.yourdomain.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // Backup certificate
.build()
private val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("User-Agent", "YourApp/1.0")
.build()
chain.proceed(request)
}
.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
// Get certificate hash from command line:
// openssl s_client -connect api.yourdomain.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64Biometric Authentication (Kotlin)
// BiometricAuth.kt
import android.content.Context
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
class BiometricAuth(private val activity: FragmentActivity) {
enum class BiometricType {
FINGERPRINT, FACE, IRIS, NONE
}
// MARK: - Check Biometric Availability
fun checkBiometricSupport(): BiometricType {
val biometricManager = BiometricManager.from(activity)
return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
// Device has biometric capability
BiometricType.FINGERPRINT // Could be fingerprint, face, or iris
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricType.NONE
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> BiometricType.NONE
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricType.NONE
else -> BiometricType.NONE
}
}
// MARK: - Authenticate
fun authenticate(
title: String = "Biometric Authentication",
subtitle: String = "Authenticate to continue",
onSuccess: () -> Unit,
onError: (String) -> Unit
) {
val executor = ContextCompat.getMainExecutor(activity)
val biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onError(errString.toString())
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onError("Authentication failed")
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
biometricPrompt.authenticate(promptInfo)
}
}
// Usage
val biometricAuth = BiometricAuth(activity)
if (biometricAuth.checkBiometricSupport() != BiometricAuth.BiometricType.NONE) {
biometricAuth.authenticate(
title = "Login",
subtitle = "Authenticate to access your account",
onSuccess = {
// Proceed with sensitive operation
},
onError = { error ->
Log.e("BiometricAuth", "Error: $error")
}
)
}Root Detection (Kotlin)
// RootDetection.kt
import android.os.Build
import java.io.File
object RootDetection {
// MARK: - Check if Device is Rooted
fun isRooted(): Boolean {
return checkBuildTags() || checkSuperuserApk() || checkSuBinary() || checkRWPaths()
}
// MARK: - Check Build Tags
private fun checkBuildTags(): Boolean {
val buildTags = Build.TAGS
return buildTags != null && buildTags.contains("test-keys")
}
// MARK: - Check for Superuser APK
private fun checkSuperuserApk(): Boolean {
val paths = arrayOf(
"/system/app/Superuser.apk",
"/system/app/SuperSU.apk",
"/system/app/Kinguser.apk",
"/data/app/eu.chainfire.supersu",
"/data/app/com.noshufou.android.su",
"/data/app/com.koushikdutta.superuser",
"/data/app/com.thirdparty.superuser",
"/data/app/com.yellowes.su"
)
return paths.any { File(it).exists() }
}
// MARK: - Check for SU Binary
private fun checkSuBinary(): Boolean {
val paths = arrayOf(
"/system/bin/su",
"/system/xbin/su",
"/system/sbin/su",
"/sbin/su",
"/vendor/bin/su",
"/su/bin/su"
)
return paths.any { File(it).exists() }
}
// MARK: - Check for RW Paths
private fun checkRWPaths(): Boolean {
val paths = arrayOf(
"/system",
"/system/bin",
"/system/sbin",
"/system/xbin",
"/vendor/bin",
"/sbin",
"/etc"
)
return paths.any { canWriteToPath(it) }
}
private fun canWriteToPath(path: String): Boolean {
val file = File(path)
return file.canWrite()
}
// MARK: - Handle Rooted Device
fun handleRootedDevice() {
if (isRooted()) {
// Option 1: Show warning
// Show dialog to user
// Option 2: Exit app
// exitProcess(0)
// Option 3: Disable sensitive features
// Disable payment, authentication, etc.
}
}
}
// Usage in Application class
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (RootDetection.isRooted()) {
RootDetection.handleRootedDevice()
}
}
}Security Testing
Automated Security Scanning
# .github/workflows/mobile-security.yml
name: Mobile Security Scan
on:
pull_request:
branches: [main]
jobs:
ios-security:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install MobSF CLI
run: pip install mobsf
- name: Build iOS app
run: xcodebuild -workspace App.xcworkspace -scheme App -configuration Release
- name: Run MobSF scan
run: mobsf scan App.ipa
android-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Android APK
run: ./gradlew assembleRelease
- name: Run MobSF scan
run: mobsf scan app-release.apkBest Practices Checklist
- [ ] Encrypt all sensitive data at rest
- [ ] Use platform-specific secure storage (Keychain/Keystore)
- [ ] Implement certificate pinning for API calls
- [ ] Enable biometric authentication for sensitive operations
- [ ] Implement jailbreak/root detection
- [ ] Use TLS 1.3 for all network traffic
- [ ] Validate all server responses
- [ ] Clear sensitive data from memory after use
- [ ] Implement auto-lock after inactivity
- [ ] Use secure random number generation
- [ ] Obfuscate code in production builds
- [ ] Implement anti-debugging measures
- [ ] Validate deep links and intent data
- [ ] Never log sensitive information
- [ ] Regularly update dependencies
Related Resources
Authentication Implementation Template
Copy-paste ready implementation for secure authentication with JWT, sessions, and MFA.
---
JWT Authentication (Recommended for APIs)
Dependencies
npm install bcrypt jsonwebtoken express express-rate-limitEnvironment Variables
# .env
JWT_SECRET=your-secret-key-min-32-characters
JWT_REFRESH_SECRET=your-refresh-secret-key-min-32-characters
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
BCRYPT_ROUNDS=12User Model
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
passwordHash: {
type: String,
required: true
},
name: {
type: String,
required: true
},
role: {
type: String,
enum: ['user', 'moderator', 'admin'],
default: 'user'
},
emailVerified: {
type: Boolean,
default: false
},
mfaEnabled: {
type: Boolean,
default: false
},
mfaSecret: String,
tokenVersion: {
type: Number,
default: 0
},
failedLoginAttempts: {
type: Number,
default: 0
},
lockedUntil: Date,
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);Password Utilities
// utils/password.js
const bcrypt = require('bcrypt');
const BCRYPT_ROUNDS = parseInt(process.env.BCRYPT_ROUNDS) || 12;
const hashPassword = async (password) => {
return await bcrypt.hash(password, BCRYPT_ROUNDS);
};
const verifyPassword = async (password, hash) => {
return await bcrypt.compare(password, hash);
};
const validatePasswordStrength = (password) => {
const minLength = 12;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
if (password.length < minLength) {
throw new Error(`Password must be at least ${minLength} characters`);
}
const complexityScore = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecialChar]
.filter(Boolean).length;
if (complexityScore < 3) {
throw new Error('Password must include at least 3 of: uppercase, lowercase, numbers, special characters');
}
return true;
};
module.exports = {
hashPassword,
verifyPassword,
validatePasswordStrength
};JWT Utilities
// utils/jwt.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '15m';
const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || '7d';
const generateAccessToken = (user) => {
return jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
JWT_SECRET,
{
expiresIn: JWT_EXPIRES_IN,
algorithm: 'HS256',
issuer: 'your-app',
audience: 'your-api'
}
);
};
const generateRefreshToken = (user) => {
return jwt.sign(
{
userId: user.id,
tokenVersion: user.tokenVersion
},
JWT_REFRESH_SECRET,
{
expiresIn: JWT_REFRESH_EXPIRES_IN,
algorithm: 'HS256'
}
);
};
const verifyAccessToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'your-app',
audience: 'your-api'
});
} catch (error) {
throw new Error('Invalid or expired token');
}
};
const verifyRefreshToken = (token) => {
try {
return jwt.verify(token, JWT_REFRESH_SECRET, {
algorithms: ['HS256']
});
} catch (error) {
throw new Error('Invalid or expired refresh token');
}
};
module.exports = {
generateAccessToken,
generateRefreshToken,
verifyAccessToken,
verifyRefreshToken
};Authentication Middleware
// middleware/authenticate.js
const { verifyAccessToken } = require('../utils/jwt');
const authenticate = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing authentication token' });
}
const token = authHeader.substring(7);
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: error.message });
}
};
module.exports = authenticate;Auth Routes
// routes/auth.js
const express = require('express');
const rateLimit = require('express-rate-limit');
const User = require('../models/User');
const { hashPassword, verifyPassword, validatePasswordStrength } = require('../utils/password');
const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../utils/jwt');
const router = express.Router();
// Rate limiters
const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
message: 'Too many registration attempts, please try again later'
});
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
skipSuccessfulRequests: true,
message: 'Too many login attempts, please try again later'
});
// Register
router.post('/register', registerLimiter, async (req, res) => {
try {
const { email, password, name } = req.body;
// Validate inputs
if (!email || !password || !name) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Validate password strength
validatePasswordStrength(password);
// 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,
passwordHash,
name
});
// Generate tokens
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.status(201).json({
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
},
accessToken,
refreshToken
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Login
router.post('/login', loginLimiter, async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Missing email or password' });
}
// Find user
const user = await User.findOne({ email });
// Check account lockout
if (user && user.lockedUntil && user.lockedUntil > Date.now()) {
const minutesRemaining = Math.ceil((user.lockedUntil - Date.now()) / 60000);
return res.status(429).json({
error: `Account locked. Try again in ${minutesRemaining} minutes`
});
}
// Verify password (constant-time response)
if (!user) {
await verifyPassword(password, '$2b$12$constantTimeHashValue');
return res.status(401).json({ error: 'Invalid credentials' });
}
const validPassword = await verifyPassword(password, user.passwordHash);
if (!validPassword) {
// Increment failed attempts
user.failedLoginAttempts += 1;
// Lock account after 5 failed attempts
if (user.failedLoginAttempts >= 5) {
user.lockedUntil = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes
}
await user.save();
return res.status(401).json({ error: 'Invalid credentials' });
}
// Reset failed attempts on successful login
user.failedLoginAttempts = 0;
user.lockedUntil = null;
await user.save();
// Generate tokens
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.json({
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
},
accessToken,
refreshToken
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Refresh token
router.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Missing refresh token' });
}
// Verify refresh token
const payload = verifyRefreshToken(refreshToken);
// Find user
const user = await User.findById(payload.userId);
if (!user || user.tokenVersion !== payload.tokenVersion) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate new access token
const accessToken = generateAccessToken(user);
res.json({ accessToken });
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// Logout (invalidate all tokens)
router.post('/logout', async (req, res) => {
try {
const { userId } = req.body;
// Increment token version to invalidate all existing tokens
await User.findByIdAndUpdate(userId, {
$inc: { tokenVersion: 1 }
});
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;Usage Example
// app.js
const express = require('express');
const authRoutes = require('./routes/auth');
const authenticate = require('./middleware/authenticate');
const app = express();
app.use(express.json());
// Auth routes (public)
app.use('/api/auth', authRoutes);
// Protected routes
app.get('/api/profile', authenticate, async (req, res) => {
const user = await User.findById(req.user.userId);
res.json({
id: user.id,
email: user.email,
name: user.name,
role: user.role
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});---
Multi-Factor Authentication (MFA) Extension
Additional Dependencies
npm install speakeasy qrcodeMFA Routes
// routes/mfa.js
const express = require('express');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const User = require('../models/User');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Enable MFA (generate secret)
router.post('/enable', authenticate, async (req, res) => {
try {
const user = await User.findById(req.user.userId);
// Generate secret
const secret = speakeasy.generateSecret({
name: `YourApp (${user.email})`,
length: 32
});
// Store secret (not enabled until verified)
user.mfaSecret = secret.base32;
await user.save();
// Generate QR code
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
res.json({
secret: secret.base32,
qrCode
});
} catch (error) {
res.status(500).json({ error: 'Failed to enable MFA' });
}
});
// Verify and activate MFA
router.post('/verify', authenticate, async (req, res) => {
try {
const { token } = req.body;
const user = await User.findById(req.user.userId);
const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token,
window: 2
});
if (!verified) {
return res.status(400).json({ error: 'Invalid MFA code' });
}
// Activate MFA
user.mfaEnabled = true;
await user.save();
res.json({ message: 'MFA enabled successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to verify MFA' });
}
});
// Disable MFA
router.post('/disable', authenticate, async (req, res) => {
try {
const { password } = req.body;
const user = await User.findById(req.user.userId);
// Verify password before disabling MFA
const validPassword = await verifyPassword(password, user.passwordHash);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid password' });
}
user.mfaEnabled = false;
user.mfaSecret = null;
await user.save();
res.json({ message: 'MFA disabled successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to disable MFA' });
}
});
module.exports = router;Updated Login with MFA
// Add to routes/auth.js login endpoint
// After password verification:
if (validPassword) {
// Check if MFA is enabled
if (user.mfaEnabled) {
const { mfaToken } = req.body;
if (!mfaToken) {
return res.status(400).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 code' });
}
}
// Continue with token generation...
}---
Testing
// tests/auth.test.js
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');
describe('Authentication', () => {
beforeEach(async () => {
await User.deleteMany({});
});
test('Register user', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
});
expect(res.status).toBe(201);
expect(res.body).toHaveProperty('accessToken');
expect(res.body.user.email).toBe('test@example.com');
});
test('Login with valid credentials', async () => {
// Create user
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
});
// Login
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'SecurePass123!'
});
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('accessToken');
});
test('Reject weak password', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'weak',
name: 'Test User'
});
expect(res.status).toBe(400);
});
test('Lock account after failed attempts', async () => {
// Create user
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
});
// 5 failed login attempts
for (let i = 0; i < 5; i++) {
await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'WrongPassword'
});
}
// 6th attempt should be locked
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'SecurePass123!'
});
expect(res.status).toBe(429);
expect(res.body.error).toContain('Account locked');
});
});---
Security Checklist
- [ ] Password minimum 12 characters with complexity requirements
- [ ] bcrypt with cost factor 12+
- [ ] JWT with short expiration (15 minutes)
- [ ] Refresh tokens with longer expiration (7 days)
- [ ] Rate limiting on auth endpoints
- [ ] Account lockout after 5 failed attempts
- [ ] Constant-time password comparison
- [ ] Token version for global logout
- [ ] MFA support
- [ ] Security logging for all auth events
- [ ] HTTPS only in production
- [ ] Secure cookie settings (httpOnly, secure, sameSite)
Authorization Implementation Template
Copy-paste ready implementation for RBAC, ABAC, and permission-based authorization.
---
Role-Based Access Control (RBAC)
Roles and Permissions Definition
// config/permissions.js
// Define permissions
const PERMISSIONS = {
// User permissions
USERS_READ: 'users:read',
USERS_WRITE: 'users:write',
USERS_DELETE: 'users:delete',
// Post permissions
POSTS_READ: 'posts:read',
POSTS_WRITE: 'posts:write',
POSTS_DELETE: 'posts:delete',
POSTS_PUBLISH: 'posts:publish',
// Settings permissions
SETTINGS_READ: 'settings:read',
SETTINGS_WRITE: 'settings:write',
// Billing permissions
BILLING_READ: 'billing:read',
BILLING_WRITE: 'billing:write'
};
// Define roles
const ROLES = {
ADMIN: 'admin',
MODERATOR: 'moderator',
EDITOR: 'editor',
USER: 'user',
GUEST: 'guest'
};
// Role-permission mapping
const ROLE_PERMISSIONS = {
[ROLES.ADMIN]: [
PERMISSIONS.USERS_READ,
PERMISSIONS.USERS_WRITE,
PERMISSIONS.USERS_DELETE,
PERMISSIONS.POSTS_READ,
PERMISSIONS.POSTS_WRITE,
PERMISSIONS.POSTS_DELETE,
PERMISSIONS.POSTS_PUBLISH,
PERMISSIONS.SETTINGS_READ,
PERMISSIONS.SETTINGS_WRITE,
PERMISSIONS.BILLING_READ,
PERMISSIONS.BILLING_WRITE
],
[ROLES.MODERATOR]: [
PERMISSIONS.USERS_READ,
PERMISSIONS.POSTS_READ,
PERMISSIONS.POSTS_WRITE,
PERMISSIONS.POSTS_DELETE
],
[ROLES.EDITOR]: [
PERMISSIONS.POSTS_READ,
PERMISSIONS.POSTS_WRITE,
PERMISSIONS.POSTS_PUBLISH
],
[ROLES.USER]: [
PERMISSIONS.POSTS_READ,
PERMISSIONS.POSTS_WRITE
],
[ROLES.GUEST]: [
PERMISSIONS.POSTS_READ
]
};
// Helper functions
const getRolePermissions = (role) => {
return ROLE_PERMISSIONS[role] || [];
};
const hasPermission = (role, permission) => {
const permissions = getRolePermissions(role);
return permissions.includes(permission);
};
const hasAnyPermission = (role, requiredPermissions) => {
const permissions = getRolePermissions(role);
return requiredPermissions.some(p => permissions.includes(p));
};
const hasAllPermissions = (role, requiredPermissions) => {
const permissions = getRolePermissions(role);
return requiredPermissions.every(p => permissions.includes(p));
};
module.exports = {
PERMISSIONS,
ROLES,
ROLE_PERMISSIONS,
getRolePermissions,
hasPermission,
hasAnyPermission,
hasAllPermissions
};Authorization Middleware
// middleware/authorize.js
const { hasPermission, hasAllPermissions } = require('../config/permissions');
// Require specific role(s)
const requireRole = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions',
required: allowedRoles,
current: req.user.role
});
}
next();
};
};
// Require specific permission(s)
const requirePermission = (...requiredPermissions) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const userPermissions = getRolePermissions(req.user.role);
const hasRequiredPermissions = requiredPermissions.every(
permission => userPermissions.includes(permission)
);
if (!hasRequiredPermissions) {
return res.status(403).json({
error: 'Insufficient permissions',
required: requiredPermissions,
current: userPermissions
});
}
next();
};
};
// Require any of the specified permissions
const requireAnyPermission = (...permissions) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const userPermissions = getRolePermissions(req.user.role);
const hasAnyPermission = permissions.some(
permission => userPermissions.includes(permission)
);
if (!hasAnyPermission) {
return res.status(403).json({
error: 'Insufficient permissions',
required: permissions,
current: userPermissions
});
}
next();
};
};
// Resource ownership check
const requireOwnership = (getResourceOwnerId) => {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
// Admins bypass ownership check
if (req.user.role === 'admin') {
return next();
}
try {
const ownerId = await getResourceOwnerId(req);
if (ownerId !== req.user.userId) {
return res.status(403).json({ error: 'Not authorized to access this resource' });
}
next();
} catch (error) {
res.status(500).json({ error: 'Authorization check failed' });
}
};
};
module.exports = {
requireRole,
requirePermission,
requireAnyPermission,
requireOwnership
};Usage Examples
// routes/users.js
const express = require('express');
const authenticate = require('../middleware/authenticate');
const { requireRole, requirePermission } = require('../middleware/authorize');
const { PERMISSIONS, ROLES } = require('../config/permissions');
const router = express.Router();
// List users (requires users:read permission)
router.get('/',
authenticate,
requirePermission(PERMISSIONS.USERS_READ),
async (req, res) => {
const users = await User.find();
res.json(users);
}
);
// Create user (requires users:write permission)
router.post('/',
authenticate,
requirePermission(PERMISSIONS.USERS_WRITE),
async (req, res) => {
const user = await User.create(req.body);
res.json(user);
}
);
// Delete user (admin only)
router.delete('/:id',
authenticate,
requireRole(ROLES.ADMIN),
async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.json({ message: 'User deleted' });
}
);
module.exports = router;// routes/posts.js
const express = require('express');
const authenticate = require('../middleware/authenticate');
const { requirePermission, requireOwnership } = require('../middleware/authorize');
const { PERMISSIONS } = require('../config/permissions');
const router = express.Router();
// Public: Read posts
router.get('/', async (req, res) => {
const posts = await Post.find({ published: true });
res.json(posts);
});
// Authenticated: Create post
router.post('/',
authenticate,
requirePermission(PERMISSIONS.POSTS_WRITE),
async (req, res) => {
const post = await Post.create({
...req.body,
authorId: req.user.userId
});
res.json(post);
}
);
// Owner or moderator: Update post
router.put('/:id',
authenticate,
requireAnyPermission(PERMISSIONS.POSTS_WRITE, PERMISSIONS.POSTS_DELETE),
requireOwnership(async (req) => {
const post = await Post.findById(req.params.id);
return post.authorId;
}),
async (req, res) => {
const post = await Post.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json(post);
}
);
// Moderator or admin: Delete post
router.delete('/:id',
authenticate,
requirePermission(PERMISSIONS.POSTS_DELETE),
async (req, res) => {
await Post.findByIdAndDelete(req.params.id);
res.json({ message: 'Post deleted' });
}
);
module.exports = router;---
Attribute-Based Access Control (ABAC)
Policy Engine
// utils/policyEngine.js
class PolicyEngine {
constructor() {
this.policies = [];
}
addPolicy(policy) {
this.policies.push(policy);
}
async evaluate(context) {
for (const policy of this.policies) {
const result = await policy.evaluate(context);
// Explicit allow
if (result === 'allow') {
return true;
}
// Explicit deny (takes precedence)
if (result === 'deny') {
return false;
}
// Continue to next policy
}
// Deny by default
return false;
}
}
// Base policy class
class Policy {
constructor(name) {
this.name = name;
}
async evaluate(context) {
throw new Error('evaluate() must be implemented');
}
}
module.exports = { PolicyEngine, Policy };Example Policies
// policies/adminPolicy.js
const { Policy } = require('../utils/policyEngine');
class AdminPolicy extends Policy {
constructor() {
super('AdminPolicy');
}
async evaluate(context) {
// Admins can do anything
if (context.user.role === 'admin') {
return 'allow';
}
return 'continue';
}
}
module.exports = AdminPolicy;// policies/ownershipPolicy.js
const { Policy } = require('../utils/policyEngine');
class OwnershipPolicy extends Policy {
constructor() {
super('OwnershipPolicy');
}
async evaluate(context) {
const { user, resource, action } = context;
// Owners can edit/delete their own resources
if (['edit', 'delete'].includes(action)) {
if (resource.ownerId === user.userId) {
return 'allow';
}
return 'deny';
}
return 'continue';
}
}
module.exports = OwnershipPolicy;// policies/timeBasedPolicy.js
const { Policy } = require('../utils/policyEngine');
class TimeBasedPolicy extends Policy {
constructor() {
super('TimeBasedPolicy');
}
async evaluate(context) {
const { user, action } = context;
// Sensitive operations only during business hours (9 AM - 5 PM)
if (action === 'delete' && user.role !== 'admin') {
const hour = new Date().getHours();
if (hour < 9 || hour > 17) {
return 'deny';
}
}
return 'continue';
}
}
module.exports = TimeBasedPolicy;// policies/departmentPolicy.js
const { Policy } = require('../utils/policyEngine');
class DepartmentPolicy extends Policy {
constructor() {
super('DepartmentPolicy');
}
async evaluate(context) {
const { user, resource, action } = context;
// Users can only access resources from their department
if (action === 'read') {
if (resource.department === user.department) {
return 'allow';
}
return 'deny';
}
return 'continue';
}
}
module.exports = DepartmentPolicy;Initialize Policy Engine
// config/authorization.js
const { PolicyEngine } = require('../utils/policyEngine');
const AdminPolicy = require('../policies/adminPolicy');
const OwnershipPolicy = require('../policies/ownershipPolicy');
const TimeBasedPolicy = require('../policies/timeBasedPolicy');
const DepartmentPolicy = require('../policies/departmentPolicy');
const policyEngine = new PolicyEngine();
// Add policies (order matters - earlier policies take precedence)
policyEngine.addPolicy(new AdminPolicy());
policyEngine.addPolicy(new OwnershipPolicy());
policyEngine.addPolicy(new TimeBasedPolicy());
policyEngine.addPolicy(new DepartmentPolicy());
module.exports = policyEngine;ABAC Middleware
// middleware/abac.js
const policyEngine = require('../config/authorization');
const authorize = (action, resourceType, getResource) => {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
// Load resource
const resource = await getResource(req);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
// Build context
const context = {
user: {
userId: req.user.userId,
email: req.user.email,
role: req.user.role,
department: req.user.department
},
resource: {
id: resource.id,
type: resourceType,
ownerId: resource.ownerId,
department: resource.department,
classification: resource.classification
},
action,
environment: {
time: new Date(),
ipAddress: req.ip,
userAgent: req.get('user-agent')
}
};
// Evaluate policy
const allowed = await policyEngine.evaluate(context);
if (!allowed) {
return res.status(403).json({ error: 'Access denied' });
}
// Attach resource to request for use in handler
req.resource = resource;
next();
} catch (error) {
res.status(500).json({ error: 'Authorization check failed' });
}
};
};
module.exports = authorize;ABAC Usage Example
// routes/documents.js
const express = require('express');
const authenticate = require('../middleware/authenticate');
const authorize = require('../middleware/abac');
const Document = require('../models/Document');
const router = express.Router();
// Read document
router.get('/:id',
authenticate,
authorize('read', 'document', async (req) => {
return await Document.findById(req.params.id);
}),
(req, res) => {
res.json(req.resource);
}
);
// Update document
router.put('/:id',
authenticate,
authorize('edit', 'document', async (req) => {
return await Document.findById(req.params.id);
}),
async (req, res) => {
const updated = await Document.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true }
);
res.json(updated);
}
);
// Delete document
router.delete('/:id',
authenticate,
authorize('delete', 'document', async (req) => {
return await Document.findById(req.params.id);
}),
async (req, res) => {
await Document.findByIdAndDelete(req.params.id);
res.json({ message: 'Document deleted' });
}
);
module.exports = router;---
Relationship-Based Access Control (ReBAC)
Permission Model
// models/ResourcePermission.js
const mongoose = require('mongoose');
const resourcePermissionSchema = new mongoose.Schema({
resourceType: {
type: String,
required: true,
enum: ['document', 'project', 'folder']
},
resourceId: {
type: String,
required: true
},
userId: {
type: String,
required: true
},
relationship: {
type: String,
required: true,
enum: ['owner', 'editor', 'viewer']
},
grantedBy: String,
grantedAt: {
type: Date,
default: Date.now
}
});
// Compound index for fast lookups
resourcePermissionSchema.index({ resourceType: 1, resourceId: 1, userId: 1 });
module.exports = mongoose.model('ResourcePermission', resourcePermissionSchema);Permission Utilities
// utils/permissions.js
const ResourcePermission = require('../models/ResourcePermission');
const RELATIONSHIPS = {
OWNER: 'owner',
EDITOR: 'editor',
VIEWER: 'viewer'
};
// Check if user has relationship to resource
const hasRelationship = async (userId, resourceType, resourceId, relationship) => {
const permission = await ResourcePermission.findOne({
userId,
resourceType,
resourceId,
relationship
});
return !!permission;
};
// Check if user has any of the specified relationships
const hasAnyRelationship = async (userId, resourceType, resourceId, relationships) => {
const permission = await ResourcePermission.findOne({
userId,
resourceType,
resourceId,
relationship: { $in: relationships }
});
return !!permission;
};
// Grant access to resource
const grantAccess = async (userId, resourceType, resourceId, relationship, grantedBy) => {
// Check if permission already exists
const existing = await ResourcePermission.findOne({
userId,
resourceType,
resourceId
});
if (existing) {
// Update existing permission
existing.relationship = relationship;
existing.grantedBy = grantedBy;
await existing.save();
return existing;
}
// Create new permission
return await ResourcePermission.create({
userId,
resourceType,
resourceId,
relationship,
grantedBy
});
};
// Revoke access to resource
const revokeAccess = async (userId, resourceType, resourceId) => {
await ResourcePermission.deleteOne({
userId,
resourceType,
resourceId
});
};
// Get all users with access to resource
const getResourceCollaborators = async (resourceType, resourceId) => {
return await ResourcePermission.find({
resourceType,
resourceId
}).populate('userId');
};
// Get all resources user has access to
const getUserResources = async (userId, resourceType) => {
return await ResourcePermission.find({
userId,
resourceType
});
};
module.exports = {
RELATIONSHIPS,
hasRelationship,
hasAnyRelationship,
grantAccess,
revokeAccess,
getResourceCollaborators,
getUserResources
};ReBAC Middleware
// middleware/rebac.js
const { hasAnyRelationship, RELATIONSHIPS } = require('../utils/permissions');
const requireRelationship = (resourceType, ...allowedRelationships) => {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const resourceId = req.params.id;
try {
const hasAccess = await hasAnyRelationship(
req.user.userId,
resourceType,
resourceId,
allowedRelationships
);
if (!hasAccess) {
return res.status(403).json({ error: 'Access denied' });
}
next();
} catch (error) {
res.status(500).json({ error: 'Authorization check failed' });
}
};
};
module.exports = { requireRelationship };ReBAC Usage Example
// routes/projects.js
const express = require('express');
const authenticate = require('../middleware/authenticate');
const { requireRelationship } = require('../middleware/rebac');
const { RELATIONSHIPS, grantAccess, revokeAccess } = require('../utils/permissions');
const router = express.Router();
// View project (owner, editor, or viewer)
router.get('/:id',
authenticate,
requireRelationship('project',
RELATIONSHIPS.OWNER,
RELATIONSHIPS.EDITOR,
RELATIONSHIPS.VIEWER
),
async (req, res) => {
const project = await Project.findById(req.params.id);
res.json(project);
}
);
// Edit project (owner or editor)
router.put('/:id',
authenticate,
requireRelationship('project',
RELATIONSHIPS.OWNER,
RELATIONSHIPS.EDITOR
),
async (req, res) => {
const project = await Project.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true }
);
res.json(project);
}
);
// Delete project (owner only)
router.delete('/:id',
authenticate,
requireRelationship('project', RELATIONSHIPS.OWNER),
async (req, res) => {
await Project.findByIdAndDelete(req.params.id);
res.json({ message: 'Project deleted' });
}
);
// Share project (owner only)
router.post('/:id/share',
authenticate,
requireRelationship('project', RELATIONSHIPS.OWNER),
async (req, res) => {
const { userId, relationship } = req.body;
await grantAccess(
userId,
'project',
req.params.id,
relationship,
req.user.userId
);
res.json({ message: 'Access granted' });
}
);
// Revoke access (owner only)
router.delete('/:id/share/:userId',
authenticate,
requireRelationship('project', RELATIONSHIPS.OWNER),
async (req, res) => {
await revokeAccess(req.params.userId, 'project', req.params.id);
res.json({ message: 'Access revoked' });
}
);
module.exports = router;---
Testing
// tests/authorization.test.js
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');
const Post = require('../models/Post');
describe('Authorization', () => {
let adminToken, userToken, moderatorToken;
let adminUser, regularUser, moderatorUser;
beforeEach(async () => {
// Create users
adminUser = await User.create({
email: 'admin@example.com',
passwordHash: await hashPassword('password'),
role: 'admin'
});
regularUser = await User.create({
email: 'user@example.com',
passwordHash: await hashPassword('password'),
role: 'user'
});
moderatorUser = await User.create({
email: 'moderator@example.com',
passwordHash: await hashPassword('password'),
role: 'moderator'
});
// Generate tokens
adminToken = generateAccessToken(adminUser);
userToken = generateAccessToken(regularUser);
moderatorToken = generateAccessToken(moderatorUser);
});
test('Admin can delete any post', async () => {
const post = await Post.create({
title: 'Test Post',
authorId: regularUser.id
});
const res = await request(app)
.delete(`/api/posts/${post.id}`)
.set('Authorization', `Bearer ${adminToken}`);
expect(res.status).toBe(200);
});
test('Regular user cannot delete others posts', async () => {
const post = await Post.create({
title: 'Test Post',
authorId: adminUser.id
});
const res = await request(app)
.delete(`/api/posts/${post.id}`)
.set('Authorization', `Bearer ${userToken}`);
expect(res.status).toBe(403);
});
test('Moderator can delete any post', async () => {
const post = await Post.create({
title: 'Test Post',
authorId: regularUser.id
});
const res = await request(app)
.delete(`/api/posts/${post.id}`)
.set('Authorization', `Bearer ${moderatorToken}`);
expect(res.status).toBe(200);
});
});---
Security Checklist
- [ ] Deny by default authorization
- [ ] Check authorization on every request
- [ ] Don't cache authorization decisions
- [ ] Log authorization failures
- [ ] Test vertical and horizontal privilege escalation
- [ ] Use indirect object references (UUIDs)
- [ ] Implement proper error messages (don't leak info)
- [ ] Consider using ABAC for complex scenarios
- [ ] Document permission model clearly
- [ ] Regular audit of permissions
Common Vulnerabilities Catalog
Comprehensive catalog of common security vulnerabilities with prevention strategies.
---
Path Traversal (Directory Traversal)
Attack: Access files outside intended directory using ../ sequences.
Vulnerable Code
// Bad: No path validation
app.get('/files/:filename', (req, res) => {
const filepath = path.join('/uploads', req.params.filename);
res.sendFile(filepath);
});
// Attack: GET /files/../../etc/passwd
// Result: Reads system password fileSecure Code
// Good: Validate and sanitize path
const getFile = (filename) => {
// Remove path separators and null bytes
const clean = path.basename(filename).replace(/\0/g, '');
if (clean !== filename || clean.length === 0) {
throw new ValidationError('Invalid filename');
}
// Resolve to absolute path and verify it's in allowed directory
const uploadsDir = path.resolve('/uploads');
const filepath = path.resolve(uploadsDir, clean);
if (!filepath.startsWith(uploadsDir)) {
throw new SecurityError('Path traversal detected');
}
// Verify file exists and is a file (not directory)
const stats = fs.statSync(filepath);
if (!stats.isFile()) {
throw new ValidationError('Not a file');
}
return filepath;
};
app.get('/files/:filename', (req, res) => {
try {
const filepath = getFile(req.params.filename);
res.sendFile(filepath);
} catch (error) {
res.status(400).json({ error: error.message });
}
});---
Insecure Deserialization
Attack: Execute arbitrary code by manipulating serialized objects.
Vulnerable Code
// Bad: Unsafe deserialization
app.post('/api/load-config', (req, res) => {
const config = eval(req.body.config); // BAD: Extremely dangerous
res.json(config);
});
// Bad: Using pickle in Python (if attacker controls input)
// config = pickle.loads(user_input)Secure Code
// Good: Use JSON.parse with validation
const loadConfig = (configString) => {
try {
const config = JSON.parse(configString);
// Validate schema
const schema = Joi.object({
theme: Joi.string().valid('light', 'dark'),
language: Joi.string().valid('en', 'es', 'fr'),
notifications: Joi.boolean()
});
const { error, value } = schema.validate(config);
if (error) {
throw new ValidationError('Invalid configuration');
}
return value;
} catch (error) {
throw new ValidationError('Failed to parse configuration');
}
};
app.post('/api/load-config', (req, res) => {
const config = loadConfig(req.body.config);
res.json(config);
});---
XML External Entity (XXE) Injection
Attack: Reference external entities in XML to read files or perform SSRF.
Vulnerable Code
// Bad: Parse XML without disabling external entities
const xml2js = require('xml2js');
app.post('/api/upload-xml', async (req, res) => {
const parser = new xml2js.Parser(); // VULNERABLE: Default settings allow XXE
const result = await parser.parseStringPromise(req.body.xml);
res.json(result);
});
// Attack XML:
// <?xml version="1.0"?>
// <!DOCTYPE foo [
// <!ENTITY xxe SYSTEM "file:///etc/passwd">
// ]>
// <data>&xxe;</data>Secure Code
// Good: Disable external entities
const xml2js = require('xml2js');
app.post('/api/upload-xml', async (req, res) => {
const parser = new xml2js.Parser({
explicitRoot: true,
explicitArray: false,
// Disable entity expansion
xmlns: false,
// Limit nesting depth
normalize: false,
trim: true
});
try {
const result = await parser.parseStringPromise(req.body.xml);
res.json(result);
} catch (error) {
res.status(400).json({ error: 'Invalid XML' });
}
});
// Better: Use JSON instead of XML when possible---
Insecure Direct Object References (IDOR)
Attack: Access resources by manipulating IDs without authorization checks.
Vulnerable Code
// Bad: No authorization check
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findById(req.params.id);
res.json(invoice);
});
// Attack: User changes invoice ID in URL to view others' invoices
// GET /api/invoices/12345 (own invoice)
// GET /api/invoices/12346 (someone else's invoice) - VULNERABLESecure Code
// Good: Verify ownership
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findById(req.params.id);
if (!invoice) {
return res.status(404).json({ error: 'Invoice not found' });
}
// Verify user owns this invoice
if (invoice.userId !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(invoice);
});
// Better: Use UUIDs instead of sequential IDs
const invoice = await Invoice.create({
id: crypto.randomUUID(), // Random, non-guessable ID
userId: req.user.id,
amount: req.body.amount
});---
Mass Assignment
Attack: Modify unintended fields by including them in request body.
Vulnerable Code
// Bad: Accept all fields from request
app.post('/api/users', async (req, res) => {
const user = await User.create(req.body); // DANGEROUS: Accepts any field
res.json(user);
});
// Attack:
// POST /api/users
// {
// "email": "user@example.com",
// "password": "password123",
// "role": "admin" // ATTACK: Attacker sets their role to admin
// }Secure Code
// Good: Explicitly allow only specific fields
app.post('/api/users', async (req, res) => {
const allowedFields = ['email', 'password', 'name'];
const userData = {};
for (const field of allowedFields) {
if (req.body[field] !== undefined) {
userData[field] = req.body[field];
}
}
// Set secure defaults
userData.role = 'user';
userData.emailVerified = false;
const user = await User.create(userData);
res.json(user);
});
// Alternative: Use schema validation
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(12).required(),
name: Joi.string().max(100).required()
// role is not in schema, so it can't be set
});
app.post('/api/users', async (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details });
}
const user = await User.create({
...value,
role: 'user',
emailVerified: false
});
res.json(user);
});---
Server-Side Template Injection (SSTI)
Attack: Execute code by injecting template syntax.
Vulnerable Code
// Bad: User input directly in template
const ejs = require('ejs');
app.get('/greet', (req, res) => {
const name = req.query.name;
const template = `<h1>Hello <%= ${name} %></h1>`; // DANGEROUS: User input in template
const html = ejs.render(template);
res.send(html);
});
// Attack: /greet?name=process.exit()Secure Code
// Good: Pass data as variables, don't construct template from user input
app.get('/greet', (req, res) => {
const name = req.query.name;
res.render('greet', {
name: name // EJS auto-escapes by default
});
});
// greet.ejs:
// <h1>Hello <%= name %></h1>---
Race Conditions
Attack: Exploit timing window between check and use (TOCTOU).
Vulnerable Code
// Bad: Race condition in money transfer
const transferMoney = async (fromUserId, toUserId, amount) => {
const fromUser = await User.findById(fromUserId);
// Check balance
if (fromUser.balance < amount) {
throw new Error('Insufficient funds');
}
// [WARNING] Race condition window here!
// Multiple requests can pass the check before balance is updated
// Update balances
await User.findByIdAndUpdate(fromUserId, {
$inc: { balance: -amount }
});
await User.findByIdAndUpdate(toUserId, {
$inc: { balance: amount }
});
};Secure Code
// Good: Use atomic operations or transactions
const transferMoney = async (fromUserId, toUserId, amount) => {
const session = await mongoose.startSession();
try {
await session.withTransaction(async () => {
// Atomic decrement with check
const result = await User.findOneAndUpdate(
{
_id: fromUserId,
balance: { $gte: amount } // Check in same operation
},
{
$inc: { balance: -amount }
},
{ session, new: true }
);
if (!result) {
throw new Error('Insufficient funds');
}
// Atomic increment
await User.findByIdAndUpdate(
toUserId,
{ $inc: { balance: amount } },
{ session }
);
});
} finally {
session.endSession();
}
};
// Alternative: Use database-level locks
const transferMoneyWithLock = async (fromUserId, toUserId, amount) => {
// Acquire lock
const lock = await acquireLock(`transfer:${fromUserId}`);
try {
const fromUser = await User.findById(fromUserId);
if (fromUser.balance < amount) {
throw new Error('Insufficient funds');
}
await User.findByIdAndUpdate(fromUserId, {
$inc: { balance: -amount }
});
await User.findByIdAndUpdate(toUserId, {
$inc: { balance: amount }
});
} finally {
await lock.release();
}
};---
Open Redirect
Attack: Redirect users to malicious sites via URL parameter.
Vulnerable Code
// Bad: Unvalidated redirect
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.redirect(url); // VULNERABLE: Can redirect to any site
});
// Attack: /redirect?url=https://malicious-site.comSecure Code
// Good: Validate redirect URL
const isValidRedirect = (url) => {
try {
const parsed = new URL(url);
// Only allow same origin
const currentOrigin = `${req.protocol}://${req.get('host')}`;
if (parsed.origin !== currentOrigin) {
return false;
}
return true;
} catch (error) {
return false;
}
};
app.get('/redirect', (req, res) => {
const url = req.query.url;
if (!isValidRedirect(url)) {
return res.status(400).json({ error: 'Invalid redirect URL' });
}
res.redirect(url);
});
// Better: Use path-only redirects
const allowedPaths = ['/dashboard', '/profile', '/settings'];
app.get('/redirect', (req, res) => {
const path = req.query.path;
if (!allowedPaths.includes(path)) {
return res.status(400).json({ error: 'Invalid redirect path' });
}
res.redirect(path);
});---
HTTP Response Splitting
Attack: Inject CRLF characters to inject headers or create XSS.
Vulnerable Code
// Bad: Unsanitized header values
app.get('/set-language', (req, res) => {
const lang = req.query.lang;
res.setHeader('Content-Language', lang); // VULNERABLE: Can inject headers
res.send('Language set');
});
// Attack: /set-language?lang=en%0D%0ASet-Cookie:%20admin=true
// Injects: Content-Language: en\r\nSet-Cookie: admin=trueSecure Code
// Good: Validate and sanitize header values
const sanitizeHeaderValue = (value) => {
// Remove CRLF characters
return value.replace(/[\r\n]/g, '');
};
app.get('/set-language', (req, res) => {
const lang = req.query.lang;
// Validate against allowlist
const allowedLanguages = ['en', 'es', 'fr', 'de'];
if (!allowedLanguages.includes(lang)) {
return res.status(400).json({ error: 'Invalid language' });
}
res.setHeader('Content-Language', lang);
res.send('Language set');
});---
Clickjacking
Attack: Trick users into clicking on hidden elements via iframe.
Vulnerable Code
<!-- No protection against framing -->
<!DOCTYPE html>
<html>
<body>
<h1>Transfer Money</h1>
<form action="/transfer" method="POST">
<button type="submit">Confirm Transfer</button>
</form>
</body>
</html>Secure Code
// Good: X-Frame-Options header
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
// Or: 'SAMEORIGIN' to allow framing by same origin
next();
});
// Better: Content-Security-Policy frame-ancestors
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
next();
});
// Using Helmet
const helmet = require('helmet');
app.use(helmet({
frameguard: { action: 'deny' }
}));---
Insufficient Logging
Attack: Perform malicious actions without detection.
Vulnerable Code
// Bad: No logging
app.post('/api/transfer', authenticate, async (req, res) => {
await transferMoney(req.user.id, req.body.recipientId, req.body.amount);
res.json({ success: true });
});Secure Code
// Good: Comprehensive security logging
const logger = require('winston');
const securityLogger = logger.createLogger({
level: 'info',
format: logger.format.combine(
logger.format.timestamp(),
logger.format.json()
),
transports: [
new logger.transports.File({ filename: 'security.log' }),
new logger.transports.Console()
]
});
app.post('/api/transfer', authenticate, async (req, res) => {
const { recipientId, amount } = req.body;
// Log before action
securityLogger.info('Transfer initiated', {
userId: req.user.id,
recipientId,
amount,
ip: req.ip,
userAgent: req.get('user-agent'),
timestamp: new Date().toISOString()
});
try {
await transferMoney(req.user.id, recipientId, amount);
// Log success
securityLogger.info('Transfer completed', {
userId: req.user.id,
recipientId,
amount
});
res.json({ success: true });
} catch (error) {
// Log failure
securityLogger.warn('Transfer failed', {
userId: req.user.id,
recipientId,
amount,
error: error.message
});
res.status(400).json({ error: error.message });
}
});
// Log authentication failures
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
try {
const user = await authenticateUser(email, password);
securityLogger.info('Login successful', {
userId: user.id,
email,
ip: req.ip
});
res.json({ token: generateToken(user) });
} catch (error) {
securityLogger.warn('Login failed', {
email,
ip: req.ip,
reason: 'invalid_credentials'
});
res.status(401).json({ error: 'Invalid credentials' });
}
});---
Sensitive Data Exposure
Attack: Access sensitive data through logs, errors, or insecure storage.
Vulnerable Code
// Bad: Logging sensitive data
logger.info('User registered', {
email: user.email,
password: user.password, // BAD: Never log passwords
ssn: user.ssn, // BAD: Never log PII
creditCard: user.creditCard // BAD: Never log payment info
});
// Bad: Returning sensitive data in API
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // BAD: Returns password hash, tokens, etc.
});Secure Code
// Good: Sanitize logs
const sanitizeForLogging = (data) => {
const sanitized = { ...data };
const sensitiveFields = ['password', 'passwordHash', 'ssn', 'creditCard', 'token'];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
};
logger.info('User registered', sanitizeForLogging({
email: user.email,
password: user.password
}));
// Good: Return only necessary fields
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json({
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt
// Exclude: passwordHash, tokens, etc.
});
});
// Better: Use serializers
class UserSerializer {
static serialize(user) {
return {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt
};
}
}
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(UserSerializer.serialize(user));
});---
Unvalidated Redirects and Forwards
Attack: Use application as proxy to bypass security controls.
Vulnerable Code
// Bad: Forward requests without validation
app.get('/proxy', async (req, res) => {
const url = req.query.url;
const response = await fetch(url); // VULNERABLE: SSRF vulnerability
const data = await response.text();
res.send(data);
});
// Attack: /proxy?url=http://localhost:6379/
// Can access internal servicesSecure Code
// Good: Validate destination
const allowedDomains = ['api.example.com', 'cdn.example.com'];
const isAllowedUrl = (urlString) => {
try {
const url = new URL(urlString);
// Check protocol
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
// Check domain allowlist
if (!allowedDomains.includes(url.hostname)) {
return false;
}
return true;
} catch (error) {
return false;
}
};
app.get('/proxy', async (req, res) => {
const url = req.query.url;
if (!isAllowedUrl(url)) {
return res.status(400).json({ error: 'Invalid URL' });
}
const response = await fetch(url, {
redirect: 'manual', // Prevent redirect-based bypass
timeout: 5000
});
const data = await response.text();
res.send(data);
});---
References
Related skills
FAQ
Which standards does it align to?
OWASP Top 10:2025, OWASP API Security Top 10 (2023), and the NIST SSDF secure SDLC baseline.
What auth method does it recommend first?
Passkeys/WebAuthn as primary auth for new apps in 2026, with OAuth 2.1 + PKCE and short-lived JWTs as alternatives.